Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/pandoc/datadir/luacov/linescanner.lua
12923 views
1
local LineScanner = {}
2
LineScanner.__index = LineScanner
3
4
function LineScanner:new()
5
return setmetatable({
6
first = true,
7
comment = false,
8
after_function = false,
9
enabled = true
10
}, self)
11
end
12
13
-- Raw version of string.gsub
14
local function replace(s, old, new)
15
old = old:gsub("%p", "%%%0")
16
new = new:gsub("%%", "%%%%")
17
return (s:gsub(old, new))
18
end
19
20
local fixups = {
21
{ "=", " ?= ?" }, -- '=' may be surrounded by spaces
22
{ "(", " ?%( ?" }, -- '(' may be surrounded by spaces
23
{ ")", " ?%) ?" }, -- ')' may be surrounded by spaces
24
{ "<FULLID>", "x ?[%[%.]? ?[ntfx0']* ?%]?" }, -- identifier, possibly indexed once
25
{ "<IDS>", "x ?, ?x[x, ]*" }, -- at least two comma-separated identifiers
26
{ "<FIELDNAME>", "%[? ?[ntfx0']+ ?%]?" }, -- field, possibly like ["this"]
27
{ "<PARENS>", "[ %(]*" }, -- optional opening parentheses
28
}
29
30
-- Utility function to make patterns more readable
31
local function fixup(pat)
32
for _, fixup_pair in ipairs(fixups) do
33
pat = replace(pat, fixup_pair[1], fixup_pair[2])
34
end
35
36
return pat
37
end
38
39
--- Lines that are always excluded from accounting
40
local any_hits_exclusions = {
41
"", -- Empty line
42
"end[,; %)]*", -- Single "end"
43
"else", -- Single "else"
44
"repeat", -- Single "repeat"
45
"do", -- Single "do"
46
"if", -- Single "if"
47
"then", -- Single "then"
48
"while t do", -- "while true do" generates no code
49
"if t then", -- "if true then" generates no code
50
"local x", -- "local var"
51
fixup "local x=", -- "local var ="
52
fixup "local <IDS>", -- "local var1, ..., varN"
53
fixup "local <IDS>=", -- "local var1, ..., varN ="
54
"local function x", -- "local function f (arg1, ..., argN)"
55
}
56
57
--- Lines that are only excluded from accounting when they have 0 hits
58
local zero_hits_exclusions = {
59
"[ntfx0',= ]+,", -- "var1 var2," multi columns table stuff
60
"{ ?} ?,", -- Empty table before comma leaves no trace in tables and calls
61
fixup "<FIELDNAME>=.+[,;]", -- "[123] = 23," "['foo'] = "asd","
62
fixup "<FIELDNAME>=function", -- "[123] = function(...)"
63
fixup "<FIELDNAME>=<PARENS>'", -- "[123] = [[", possibly with opening parens
64
"return function", -- "return function(arg1, ..., argN)"
65
"function", -- "function(arg1, ..., argN)"
66
"[ntfx0]", -- Single token expressions leave no trace in tables, function calls and sometimes assignments
67
"''", -- Same for strings
68
"{ ?}", -- Same for empty tables
69
fixup "<FULLID>", -- Same for local variables indexed once
70
fixup "local x=function", -- "local a = function(arg1, ..., argN)"
71
fixup "local x=<PARENS>'", -- "local a = [[", possibly with opening parens
72
fixup "local x=(<PARENS>", -- "local a = (", possibly with several parens
73
fixup "local <IDS>=(<PARENS>", -- "local a, b = (", possibly with several parens
74
fixup "local x=n", -- "local a = nil; local b = nil" produces no trace for the second statement
75
fixup "<FULLID>=<PARENS>'", -- "a.b = [[", possibly with opening parens
76
fixup "<FULLID>=function", -- "a = function(arg1, ..., argN)"
77
"} ?,", -- "}," generates no trace if the table ends with a key-value pair
78
"} ?, ?function", -- same with "}, function(...)"
79
"break", -- "break" generates no trace in Lua 5.2+
80
"{", -- "{" opening table
81
"}?[ %)]*", -- optional closing paren, possibly with several closing parens
82
"[ntf0']+ ?}[ %)]*", -- a constant at the end of a table, possibly with closing parens (for LuaJIT)
83
"goto [%w_]+", -- goto statements
84
"::[%w_]+::", -- labels
85
}
86
87
local function excluded(exclusions, line)
88
for _, e in ipairs(exclusions) do
89
if line:match("^ *"..e.." *$") then
90
return true
91
end
92
end
93
94
return false
95
end
96
97
function LineScanner:find(pattern)
98
return self.line:find(pattern, self.i)
99
end
100
101
-- Skips string literal with quote stored as self.quote.
102
-- @return boolean indicating success.
103
function LineScanner:skip_string()
104
-- Look for closing quote, possibly after even number of backslashes.
105
local _, quote_i = self:find("^(\\*)%1"..self.quote)
106
if not quote_i then
107
_, quote_i = self:find("[^\\](\\*)%1"..self.quote)
108
end
109
110
if quote_i then
111
self.i = quote_i + 1
112
self.quote = nil
113
table.insert(self.simple_line_buffer, "'")
114
return true
115
else
116
return false
117
end
118
end
119
120
-- Skips long string literal with equal signs stored as self.equals.
121
-- @return boolean indicating success.
122
function LineScanner:skip_long_string()
123
local _, bracket_i = self:find("%]"..self.equals.."%]")
124
125
if bracket_i then
126
self.i = bracket_i + 1
127
self.equals = nil
128
129
if self.comment then
130
self.comment = false
131
else
132
table.insert(self.simple_line_buffer, "'")
133
end
134
135
return true
136
else
137
return false
138
end
139
end
140
141
-- Skips function arguments.
142
-- @return boolean indicating success.
143
function LineScanner:skip_args()
144
local _, paren_i = self:find("%)")
145
146
if paren_i then
147
self.i = paren_i + 1
148
self.args = nil
149
return true
150
else
151
return false
152
end
153
end
154
155
function LineScanner:skip_whitespace()
156
local next_i = self:find("%S") or #self.line + 1
157
158
if next_i ~= self.i then
159
self.i = next_i
160
table.insert(self.simple_line_buffer, " ")
161
end
162
end
163
164
function LineScanner:skip_number()
165
if self:find("^0[xX]") then
166
self.i = self.i + 2
167
end
168
169
local _
170
_, _, self.i = self:find("^[%x%.]*()")
171
172
if self:find("^[eEpP][%+%-]") then
173
-- Skip exponent, too.
174
self.i = self.i + 2
175
_, _, self.i = self:find("^[%x%.]*()")
176
end
177
178
-- Skip LuaJIT number suffixes (i, ll, ull).
179
_, _, self.i = self:find("^[iull]*()")
180
table.insert(self.simple_line_buffer, "0")
181
end
182
183
local keywords = {["nil"] = "n", ["true"] = "t", ["false"] = "f"}
184
185
for _, keyword in ipairs({
186
"and", "break", "do", "else", "elseif", "end", "for", "function", "goto", "if",
187
"in", "local", "not", "or", "repeat", "return", "then", "until", "while"}) do
188
keywords[keyword] = keyword
189
end
190
191
function LineScanner:skip_name()
192
-- It is guaranteed that the first character matches "%a_".
193
local _, _, name = self:find("^([%w_]*)")
194
self.i = self.i + #name
195
196
if keywords[name] then
197
name = keywords[name]
198
else
199
name = "x"
200
end
201
202
table.insert(self.simple_line_buffer, name)
203
204
if name == "function" then
205
-- This flag indicates that the next pair of parentheses (function args) must be skipped.
206
self.after_function = true
207
end
208
end
209
210
-- Source lines can be explicitly ignored using `enable` and `disable` inline options.
211
-- An inline option is a simple comment: `-- luacov: enable` or `-- luacov: disable`.
212
-- Inline option parsing is not whitespace sensitive.
213
-- All lines starting from a line containing `disable` option and up to a line containing `enable`
214
-- option (or end of file) are excluded.
215
216
function LineScanner:check_inline_options(comment_body)
217
if comment_body:find("^%s*luacov:%s*enable%s*$") then
218
self.enabled = true
219
elseif comment_body:find("^%s*luacov:%s*disable%s*$") then
220
self.enabled = false
221
end
222
end
223
224
-- Consumes and analyzes a line.
225
-- @return boolean indicating whether line must be excluded.
226
-- @return boolean indicating whether line must be excluded if not hit.
227
function LineScanner:consume(line)
228
if self.first then
229
self.first = false
230
231
if line:match("^#!") then
232
-- Ignore Unix hash-bang magic line.
233
return true, true
234
end
235
end
236
237
self.line = line
238
-- As scanner goes through the line, it puts its simplified parts into buffer.
239
-- Punctuation is preserved. Whitespace is replaced with single space.
240
-- Literal strings are replaced with "''", so that a string literal
241
-- containing special characters does not confuse exclusion rules.
242
-- Numbers are replaced with "0".
243
-- Identifiers are replaced with "x".
244
-- Literal keywords (nil, true and false) are replaced with "n", "t" and "f",
245
-- other keywords are preserved.
246
-- Function declaration arguments are removed.
247
self.simple_line_buffer = {}
248
self.i = 1
249
250
while self.i <= #line do
251
-- One iteration of this loop handles one token, where
252
-- string literal start and end are considered distinct tokens.
253
if self.quote then
254
if not self:skip_string() then
255
-- String literal ends on another line.
256
break
257
end
258
elseif self.equals then
259
if not self:skip_long_string() then
260
-- Long string literal or comment ends on another line.
261
break
262
end
263
elseif self.args then
264
if not self:skip_args() then
265
-- Function arguments end on another line.
266
break
267
end
268
else
269
self:skip_whitespace()
270
271
if self:find("^%.%d") then
272
self.i = self.i + 1
273
end
274
275
if self:find("^%d") then
276
self:skip_number()
277
elseif self:find("^[%a_]") then
278
self:skip_name()
279
else
280
if self:find("^%-%-") then
281
self.comment = true
282
self.i = self.i + 2
283
end
284
285
local _, bracket_i, equals = self:find("^%[(=*)%[")
286
if equals then
287
self.i = bracket_i + 1
288
self.equals = equals
289
290
if not self.comment then
291
table.insert(self.simple_line_buffer, "'")
292
end
293
elseif self.comment then
294
-- Simple comment, check if it contains inline options and skip line.
295
self.comment = false
296
local comment_body = self.line:sub(self.i)
297
self:check_inline_options(comment_body)
298
break
299
else
300
local char = line:sub(self.i, self.i)
301
302
if char == "." then
303
-- Dot can't be saved as one character because of
304
-- ".." and "..." tokens and the fact that number literals
305
-- can start with one.
306
local _, _, dots = self:find("^(%.*)")
307
self.i = self.i + #dots
308
table.insert(self.simple_line_buffer, dots)
309
else
310
self.i = self.i + 1
311
312
if char == "'" or char == '"' then
313
table.insert(self.simple_line_buffer, "'")
314
self.quote = char
315
elseif self.after_function and char == "(" then
316
-- This is the opening parenthesis of function declaration args.
317
self.after_function = false
318
self.args = true
319
else
320
-- Save other punctuation literally.
321
-- This inserts an empty string when at the end of line,
322
-- which is fine.
323
table.insert(self.simple_line_buffer, char)
324
end
325
end
326
end
327
end
328
end
329
end
330
331
if not self.enabled then
332
-- Disabled by inline options, always exclude the line.
333
return true, true
334
end
335
336
local simple_line = table.concat(self.simple_line_buffer)
337
return excluded(any_hits_exclusions, simple_line), excluded(zero_hits_exclusions, simple_line)
338
end
339
340
return LineScanner
341
342