Path: blob/main/src/resources/pandoc/datadir/luacov/linescanner.lua
12923 views
local LineScanner = {}1LineScanner.__index = LineScanner23function LineScanner:new()4return setmetatable({5first = true,6comment = false,7after_function = false,8enabled = true9}, self)10end1112-- Raw version of string.gsub13local function replace(s, old, new)14old = old:gsub("%p", "%%%0")15new = new:gsub("%%", "%%%%")16return (s:gsub(old, new))17end1819local fixups = {20{ "=", " ?= ?" }, -- '=' may be surrounded by spaces21{ "(", " ?%( ?" }, -- '(' may be surrounded by spaces22{ ")", " ?%) ?" }, -- ')' may be surrounded by spaces23{ "<FULLID>", "x ?[%[%.]? ?[ntfx0']* ?%]?" }, -- identifier, possibly indexed once24{ "<IDS>", "x ?, ?x[x, ]*" }, -- at least two comma-separated identifiers25{ "<FIELDNAME>", "%[? ?[ntfx0']+ ?%]?" }, -- field, possibly like ["this"]26{ "<PARENS>", "[ %(]*" }, -- optional opening parentheses27}2829-- Utility function to make patterns more readable30local function fixup(pat)31for _, fixup_pair in ipairs(fixups) do32pat = replace(pat, fixup_pair[1], fixup_pair[2])33end3435return pat36end3738--- Lines that are always excluded from accounting39local any_hits_exclusions = {40"", -- Empty line41"end[,; %)]*", -- Single "end"42"else", -- Single "else"43"repeat", -- Single "repeat"44"do", -- Single "do"45"if", -- Single "if"46"then", -- Single "then"47"while t do", -- "while true do" generates no code48"if t then", -- "if true then" generates no code49"local x", -- "local var"50fixup "local x=", -- "local var ="51fixup "local <IDS>", -- "local var1, ..., varN"52fixup "local <IDS>=", -- "local var1, ..., varN ="53"local function x", -- "local function f (arg1, ..., argN)"54}5556--- Lines that are only excluded from accounting when they have 0 hits57local zero_hits_exclusions = {58"[ntfx0',= ]+,", -- "var1 var2," multi columns table stuff59"{ ?} ?,", -- Empty table before comma leaves no trace in tables and calls60fixup "<FIELDNAME>=.+[,;]", -- "[123] = 23," "['foo'] = "asd","61fixup "<FIELDNAME>=function", -- "[123] = function(...)"62fixup "<FIELDNAME>=<PARENS>'", -- "[123] = [[", possibly with opening parens63"return function", -- "return function(arg1, ..., argN)"64"function", -- "function(arg1, ..., argN)"65"[ntfx0]", -- Single token expressions leave no trace in tables, function calls and sometimes assignments66"''", -- Same for strings67"{ ?}", -- Same for empty tables68fixup "<FULLID>", -- Same for local variables indexed once69fixup "local x=function", -- "local a = function(arg1, ..., argN)"70fixup "local x=<PARENS>'", -- "local a = [[", possibly with opening parens71fixup "local x=(<PARENS>", -- "local a = (", possibly with several parens72fixup "local <IDS>=(<PARENS>", -- "local a, b = (", possibly with several parens73fixup "local x=n", -- "local a = nil; local b = nil" produces no trace for the second statement74fixup "<FULLID>=<PARENS>'", -- "a.b = [[", possibly with opening parens75fixup "<FULLID>=function", -- "a = function(arg1, ..., argN)"76"} ?,", -- "}," generates no trace if the table ends with a key-value pair77"} ?, ?function", -- same with "}, function(...)"78"break", -- "break" generates no trace in Lua 5.2+79"{", -- "{" opening table80"}?[ %)]*", -- optional closing paren, possibly with several closing parens81"[ntf0']+ ?}[ %)]*", -- a constant at the end of a table, possibly with closing parens (for LuaJIT)82"goto [%w_]+", -- goto statements83"::[%w_]+::", -- labels84}8586local function excluded(exclusions, line)87for _, e in ipairs(exclusions) do88if line:match("^ *"..e.." *$") then89return true90end91end9293return false94end9596function LineScanner:find(pattern)97return self.line:find(pattern, self.i)98end99100-- Skips string literal with quote stored as self.quote.101-- @return boolean indicating success.102function LineScanner:skip_string()103-- Look for closing quote, possibly after even number of backslashes.104local _, quote_i = self:find("^(\\*)%1"..self.quote)105if not quote_i then106_, quote_i = self:find("[^\\](\\*)%1"..self.quote)107end108109if quote_i then110self.i = quote_i + 1111self.quote = nil112table.insert(self.simple_line_buffer, "'")113return true114else115return false116end117end118119-- Skips long string literal with equal signs stored as self.equals.120-- @return boolean indicating success.121function LineScanner:skip_long_string()122local _, bracket_i = self:find("%]"..self.equals.."%]")123124if bracket_i then125self.i = bracket_i + 1126self.equals = nil127128if self.comment then129self.comment = false130else131table.insert(self.simple_line_buffer, "'")132end133134return true135else136return false137end138end139140-- Skips function arguments.141-- @return boolean indicating success.142function LineScanner:skip_args()143local _, paren_i = self:find("%)")144145if paren_i then146self.i = paren_i + 1147self.args = nil148return true149else150return false151end152end153154function LineScanner:skip_whitespace()155local next_i = self:find("%S") or #self.line + 1156157if next_i ~= self.i then158self.i = next_i159table.insert(self.simple_line_buffer, " ")160end161end162163function LineScanner:skip_number()164if self:find("^0[xX]") then165self.i = self.i + 2166end167168local _169_, _, self.i = self:find("^[%x%.]*()")170171if self:find("^[eEpP][%+%-]") then172-- Skip exponent, too.173self.i = self.i + 2174_, _, self.i = self:find("^[%x%.]*()")175end176177-- Skip LuaJIT number suffixes (i, ll, ull).178_, _, self.i = self:find("^[iull]*()")179table.insert(self.simple_line_buffer, "0")180end181182local keywords = {["nil"] = "n", ["true"] = "t", ["false"] = "f"}183184for _, keyword in ipairs({185"and", "break", "do", "else", "elseif", "end", "for", "function", "goto", "if",186"in", "local", "not", "or", "repeat", "return", "then", "until", "while"}) do187keywords[keyword] = keyword188end189190function LineScanner:skip_name()191-- It is guaranteed that the first character matches "%a_".192local _, _, name = self:find("^([%w_]*)")193self.i = self.i + #name194195if keywords[name] then196name = keywords[name]197else198name = "x"199end200201table.insert(self.simple_line_buffer, name)202203if name == "function" then204-- This flag indicates that the next pair of parentheses (function args) must be skipped.205self.after_function = true206end207end208209-- Source lines can be explicitly ignored using `enable` and `disable` inline options.210-- An inline option is a simple comment: `-- luacov: enable` or `-- luacov: disable`.211-- Inline option parsing is not whitespace sensitive.212-- All lines starting from a line containing `disable` option and up to a line containing `enable`213-- option (or end of file) are excluded.214215function LineScanner:check_inline_options(comment_body)216if comment_body:find("^%s*luacov:%s*enable%s*$") then217self.enabled = true218elseif comment_body:find("^%s*luacov:%s*disable%s*$") then219self.enabled = false220end221end222223-- Consumes and analyzes a line.224-- @return boolean indicating whether line must be excluded.225-- @return boolean indicating whether line must be excluded if not hit.226function LineScanner:consume(line)227if self.first then228self.first = false229230if line:match("^#!") then231-- Ignore Unix hash-bang magic line.232return true, true233end234end235236self.line = line237-- As scanner goes through the line, it puts its simplified parts into buffer.238-- Punctuation is preserved. Whitespace is replaced with single space.239-- Literal strings are replaced with "''", so that a string literal240-- containing special characters does not confuse exclusion rules.241-- Numbers are replaced with "0".242-- Identifiers are replaced with "x".243-- Literal keywords (nil, true and false) are replaced with "n", "t" and "f",244-- other keywords are preserved.245-- Function declaration arguments are removed.246self.simple_line_buffer = {}247self.i = 1248249while self.i <= #line do250-- One iteration of this loop handles one token, where251-- string literal start and end are considered distinct tokens.252if self.quote then253if not self:skip_string() then254-- String literal ends on another line.255break256end257elseif self.equals then258if not self:skip_long_string() then259-- Long string literal or comment ends on another line.260break261end262elseif self.args then263if not self:skip_args() then264-- Function arguments end on another line.265break266end267else268self:skip_whitespace()269270if self:find("^%.%d") then271self.i = self.i + 1272end273274if self:find("^%d") then275self:skip_number()276elseif self:find("^[%a_]") then277self:skip_name()278else279if self:find("^%-%-") then280self.comment = true281self.i = self.i + 2282end283284local _, bracket_i, equals = self:find("^%[(=*)%[")285if equals then286self.i = bracket_i + 1287self.equals = equals288289if not self.comment then290table.insert(self.simple_line_buffer, "'")291end292elseif self.comment then293-- Simple comment, check if it contains inline options and skip line.294self.comment = false295local comment_body = self.line:sub(self.i)296self:check_inline_options(comment_body)297break298else299local char = line:sub(self.i, self.i)300301if char == "." then302-- Dot can't be saved as one character because of303-- ".." and "..." tokens and the fact that number literals304-- can start with one.305local _, _, dots = self:find("^(%.*)")306self.i = self.i + #dots307table.insert(self.simple_line_buffer, dots)308else309self.i = self.i + 1310311if char == "'" or char == '"' then312table.insert(self.simple_line_buffer, "'")313self.quote = char314elseif self.after_function and char == "(" then315-- This is the opening parenthesis of function declaration args.316self.after_function = false317self.args = true318else319-- Save other punctuation literally.320-- This inserts an empty string when at the end of line,321-- which is fine.322table.insert(self.simple_line_buffer, char)323end324end325end326end327end328end329330if not self.enabled then331-- Disabled by inline options, always exclude the line.332return true, true333end334335local simple_line = table.concat(self.simple_line_buffer)336return excluded(any_hits_exclusions, simple_line), excluded(zero_hits_exclusions, simple_line)337end338339return LineScanner340341342