• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / Penlight / 16883016526

11 Aug 2025 02:27PM UTC coverage: 88.862% (-0.009%) from 88.871%
16883016526

Pull #498

github

web-flow
fix(*): some more Lua 5.5 fixes for constant loop variables (#499)
Pull Request #498: fix(*): some Lua 5.5 fixes for constant loop variables

23 of 24 new or added lines in 4 files covered. (95.83%)

60 existing lines in 5 files now uncovered.

5457 of 6141 relevant lines covered (88.86%)

257.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.05
/lua/pl/pretty.lua
1
--- Pretty-printing Lua tables.
2
-- Also provides a sandboxed Lua table reader and
3
-- a function to present large numbers in human-friendly format.
4
--
5
-- Dependencies: `pl.utils`, `pl.lexer`, `pl.stringx`, `debug`
6
-- @module pl.pretty
7

8
local append = table.insert
210✔
9
local concat = table.concat
210✔
10
local mfloor, mhuge = math.floor, math.huge
210✔
11
local mtype = math.type
210✔
12
local utils = require 'pl.utils'
210✔
13
local lexer = require 'pl.lexer'
210✔
14
local debug = require 'debug'
210✔
15
local quote_string = require'pl.stringx'.quote_string
210✔
16
local assert_arg = utils.assert_arg
210✔
17

18
local original_tostring = tostring
210✔
19

20
-- Calculate min and max integer supported by lua_Number
21
-- Assumptions:
22
-- 1. max_int = 2 ^ n - 1
23
-- 2. min_int = -max_int
24
-- 3. if n > max_int versions with integer support will have
25
-- integer overflow and versions without integers will lose least significant bit
26
-- Note: if lua_Integer is smaller than lua_Number mantissa string.format('%d')
27
-- can throw runtime error
28
local max_int, min_int
29
local next_cand = 1
210✔
30
while  next_cand > 0 and next_cand % 2 == 1 do
12,040✔
31
  max_int = next_cand
11,830✔
32
  min_int = -next_cand
11,830✔
33
  next_cand = next_cand * 2 + 1
11,830✔
34
end
35

36
local function is_integer(value)
37
  if _VERSION == "Lua 5.3" or _VERSION == "Lua 5.4" then
224✔
UNCOV
38
    return mtype(value) == "integer"
80✔
39
  end
40
  if value < min_int or value > max_int then
144✔
41
    return false
4✔
42
  end
43
  return math.floor(value) == value
140✔
44
end
45

46
local function is_float(value)
47
  if _VERSION == "Lua 5.3" or _VERSION == "Lua 5.4" then
14✔
UNCOV
48
    return mtype(value) == "float"
6✔
49
  end
50
  if value < min_int or value > max_int then
8✔
51
    return true
4✔
52
  end
53
  return mfloor(value) == value
4✔
54
end
55

56
-- Patch tostring to format numbers with better precision
57
-- and to produce cross-platform results for
58
-- infinite values and NaN.
59
local function tostring(value)
60
    if type(value) ~= "number" then
490✔
61
        return original_tostring(value)
248✔
62
    elseif value ~= value then
242✔
63
        return "NaN"
6✔
64
    elseif value == mhuge then
236✔
65
        return "Inf"
6✔
66
    elseif value == -mhuge then
230✔
67
        return "-Inf"
6✔
68
    elseif is_integer(value) then
296✔
69
        return ("%d"):format(value)
210✔
70
    else
71
        local res = ("%.14g"):format(value)
14✔
72
        if is_float(value) and not res:find("%.") then
18✔
73
            -- Number is internally a float but looks like an integer.
74
            -- Insert ".0" after first run of digits.
75
            res = res:gsub("%d+", "%0.0", 1)
8✔
76
        end
77
        return res
14✔
78
    end
79
end
80

81
local pretty = {}
210✔
82

83
local function save_global_env()
84
    local env = {}
24✔
85
    env.hook, env.mask, env.count = debug.gethook()
24✔
86

87
    -- env.hook is "external hook" if is a C hook function
88
    if env.hook~="external hook" then
24✔
89
        debug.sethook()
24✔
90
    end
91

92
    env.string_mt = getmetatable("")
×
93
    debug.setmetatable("", nil)
×
94
    return env
×
95
end
96

97
local function restore_global_env(env)
98
    if env then
6✔
99
        debug.setmetatable("", env.string_mt)
×
100
        if env.hook~="external hook" then
×
101
            debug.sethook(env.hook, env.mask, env.count)
×
102
        end
103
    end
104
end
105

106
--- Read a string representation of a Lua table.
107
-- This function loads and runs the string as Lua code, but bails out
108
-- if it contains a function definition.
109
-- Loaded string is executed in an empty environment.
110
-- @string s string to read in `{...}` format, possibly with some whitespace
111
-- before or after the curly braces. A single line comment may be present
112
-- at the beginning.
113
-- @return a table in case of success.
114
-- If loading the string failed, return `nil` and error message.
115
-- If executing loaded string failed, return `nil` and the error it raised.
116
function pretty.read(s)
210✔
117
    assert_arg(1,s,'string')
30✔
118
    if s:find '^%s*%-%-' then -- may start with a comment..
30✔
119
        s = s:gsub('%-%-.-\n','')
×
120
    end
121
    if not s:find '^%s*{' then return nil,"not a Lua table" end
30✔
122
    if s:find '[^\'"%w_]function[^\'"%w_]' then
30✔
123
        local tok = lexer.lua(s)
12✔
124
        for t,v in tok do
216✔
125
            if t == 'keyword' and v == 'function' then
156✔
126
                return nil,"cannot have functions in table definition"
6✔
127
            end
128
        end
129
    end
130
    s = 'return '..s
24✔
131
    local chunk,err = utils.load(s,'tbl','t',{})
24✔
132
    if not chunk then return nil,err end
24✔
133
    local global_env = save_global_env()
24✔
134
    local ok,ret = pcall(chunk)
×
135
    restore_global_env(global_env)
×
136
    if ok then return ret
24✔
137
    else
138
        return nil,ret
6✔
139
    end
140
end
141

142
--- Read a Lua chunk.
143
-- @string s Lua code.
144
-- @tab[opt] env environment used to run the code, empty by default.
145
-- @bool[opt] paranoid abort loading if any looping constructs a found in the code
146
-- and disable string methods.
147
-- @return the environment in case of success or `nil` and syntax or runtime error
148
-- if something went wrong.
149
function pretty.load (s, env, paranoid)
210✔
150
    env = env or {}
12✔
151
    if paranoid then
12✔
152
        local tok = lexer.lua(s)
6✔
153
        for t,v in tok do
32✔
154
            if t == 'keyword'
24✔
155
                and (v == 'for' or v == 'repeat' or v == 'function' or v == 'goto')
6✔
156
            then
157
                return nil,"looping not allowed"
6✔
158
            end
159
        end
160
    end
161
    local chunk,err = utils.load(s,'tbl','t',env)
6✔
162
    if not chunk then return nil,err end
6✔
163
    local global_env = paranoid and save_global_env()
6✔
164
    local ok,err = pcall(chunk)
6✔
165
    restore_global_env(global_env)
6✔
166
    if not ok then return nil,err end
6✔
167
    return env
6✔
168
end
169

170
local function quote_if_necessary (v)
171
    if not v then return ''
168✔
172
    else
173
        --AAS
174
        if v:find ' ' then v = quote_string(v) end
168✔
175
    end
176
    return v
168✔
177
end
178

179
local keywords
180

181
local function is_identifier (s)
182
    return type(s) == 'string' and s:find('^[%a_][%w_]*$') and not keywords[s]
84✔
183
end
184

185
local function quote (s)
186
    if type(s) == 'table' then
24✔
187
        return pretty.write(s,'')
×
188
    else
189
        --AAS
190
        return quote_string(s)-- ('%q'):format(tostring(s))
24✔
191
    end
192
end
193

194
local function index (numkey,key)
195
    --AAS
196
    if not numkey then
36✔
197
        key = quote(key)
32✔
198
         key = key:find("^%[") and (" " .. key .. " ") or key
24✔
199
    end
200
    return '['..key..']'
36✔
201
end
202

203

204
--- Create a string representation of a Lua table.
205
-- This function never fails, but may complain by returning an
206
-- extra value. Normally puts out one item per line, using
207
-- the provided indent; set the second parameter to an empty string
208
-- if you want output on one line.
209
--
210
-- *NOTE:* this is NOT a serialization function, not a full blown
211
-- debug function. Checkout out respectively the
212
-- [serpent](https://github.com/pkulchenko/serpent)
213
-- or [inspect](https://github.com/kikito/inspect.lua)
214
-- Lua modules for that if you need them.
215
-- @tab tbl Table to serialize to a string.
216
-- @string[opt] space The indent to use.
217
-- Defaults to two spaces; pass an empty string for no indentation.
218
-- @bool[opt] not_clever Pass `true` for plain output, e.g `{['key']=1}`.
219
-- Defaults to `false`.
220
-- @return a string
221
-- @return an optional error message
222
function pretty.write (tbl,space,not_clever)
210✔
223
    if type(tbl) ~= 'table' then
95✔
224
        local res = tostring(tbl)
14✔
225
        if type(tbl) == 'string' then return quote(tbl) end
14✔
226
        return res, 'not a table'
14✔
227
    end
228
    if not keywords then
81✔
229
        keywords = lexer.get_keywords()
19✔
230
    end
231
    local set = ' = '
81✔
232
    if space == '' then set = '=' end
81✔
233
    space = space or '  '
81✔
234
    local lines = {}
81✔
235
    local line = ''
81✔
236
    local tables = {}
81✔
237

238

239
    local function put(s)
240
        if #s > 0 then
285✔
241
            line = line..s
150✔
242
        end
243
    end
244

245
    local function putln (s)
246
        if #line > 0 then
477✔
247
            line = line..s
150✔
248
            append(lines,line)
150✔
249
            line = ''
150✔
250
        else
251
            append(lines,s)
327✔
252
        end
253
    end
254

255
    local function eat_last_comma ()
256
        local n = #lines
192✔
257
        local lastch = lines[n]:sub(-1,-1)
192✔
258
        if lastch == ',' then
192✔
259
            lines[n] = lines[n]:sub(1,-2)
222✔
260
        end
261
    end
262

263

264
    -- safe versions for iterators since 5.3+ honors metamethods that can throw
265
    -- errors
266
    local ipairs = function(t)
267
        local i = 0
111✔
268
        local ok, v
269
        local getter = function() return t[i] end
411✔
270
        return function()
271
                i = i + 1
300✔
272
                ok, v = pcall(getter)
396✔
273
                if v == nil or not ok then return end
300✔
274
                return i, t[i]
189✔
275
            end
276
    end
277
    local pairs = function(t)
278
        local k, v, ok
279
        local getter = function() return next(t, k) end
1,014✔
280
        return function()
281
                ok, k, v = pcall(getter)
1,048✔
282
                if not ok then return end
792✔
283
                return k, v
792✔
284
            end
285
    end
286

287
    local writeit
288
    writeit = function (t,oldindent,indent)
289
        local tp = type(t)
366✔
290
        if tp ~= 'string' and  tp ~= 'table' then
366✔
291
            putln(quote_if_necessary(tostring(t))..',')
326✔
292
        elseif tp == 'string' then
198✔
293
            -- if t:find('\n') then
294
            --     putln('[[\n'..t..']],')
295
            -- else
296
            --     putln(quote(t)..',')
297
            -- end
298
            --AAS
299
            putln(quote_string(t) ..",")
119✔
300
        elseif tp == 'table' then
123✔
301
            if tables[t] then
123✔
302
                putln('<cycle>,')
12✔
303
                return
12✔
304
            end
305
            tables[t] = true
111✔
306
            local newindent = indent..space
111✔
307
            putln('{')
111✔
308
            local used = {}
111✔
309
            if not not_clever then
111✔
310
                for i,val in ipairs(t) do
432✔
311
                    put(indent)
189✔
312
                    writeit(val,indent,newindent)
189✔
313
                    used[i] = true
189✔
314
                end
315
            end
316
            local ordered_keys = {}
111✔
317
            for k,v in pairs(t) do
560✔
318
               if type(k) ~= 'number' then
285✔
319
                  ordered_keys[#ordered_keys + 1] = k
84✔
320
               end
321
            end
322
            table.sort(ordered_keys, function (a, b)
222✔
323
                if type(a) == type(b) then
124✔
324
                    return tostring(a) < tostring(b)
162✔
325
                else
326
                    return type(a) < type(b)
24✔
327
                end
328
            end)
329
            local function write_entry (key, val)
330
                local tkey = type(key)
285✔
331
                local numkey = tkey == 'number'
285✔
332
                if not_clever then
285✔
333
                    key = tostring(key)
×
334
                    put(indent..index(numkey,key)..set)
×
335
                    writeit(val,indent,newindent)
×
336
                else
337
                    if not numkey or not used[key] then -- non-array indices
285✔
338
                        if tkey ~= 'string' then
96✔
339
                            key = tostring(key)
34✔
340
                        end
341
                        if numkey or not is_identifier(key) then
124✔
342
                            key = index(numkey,key)
48✔
343
                        end
344
                        put(indent..key..set)
96✔
345
                        writeit(val,indent,newindent)
96✔
346
                    end
347
                end
348
            end
349
            for i = 1, #ordered_keys do
195✔
350
                local key = ordered_keys[i]
84✔
351
                local val = t[key]
84✔
352
                write_entry(key, val)
84✔
353
            end
354
            for key,val in pairs(t) do
560✔
355
               if type(key) == 'number' then
285✔
356
                  write_entry(key, val)
201✔
357
               end
358
            end
359
            tables[t] = nil
111✔
360
            eat_last_comma()
111✔
361
            putln(oldindent..'},')
147✔
362
        else
363
            putln(tostring(t)..',')
×
364
        end
365
    end
366
    writeit(tbl,'',space)
81✔
367
    eat_last_comma()
81✔
368
    return concat(lines,#space > 0 and '\n' or '')
81✔
369
end
370

371
--- Dump a Lua table out to a file or stdout.
372
-- @tab t The table to write to a file or stdout.
373
-- @string[opt] filename File name to write too. Defaults to writing
374
-- to stdout.
375
function pretty.dump (t, filename)
210✔
376
    if not filename then
×
377
        print(pretty.write(t))
×
378
        return true
×
379
    else
380
        return utils.writefile(filename, pretty.write(t))
×
381
    end
382
end
383

384
--- Dump a series of arguments to stdout for debug purposes.
385
-- This function is attached to the module table `__call` method, to make it
386
-- extra easy to access. So the full:
387
--
388
--     print(require("pl.pretty").write({...}))
389
--
390
-- Can be shortened to:
391
--
392
--     require"pl.pretty" (...)
393
--
394
-- Any `nil` entries will be printed as `"<nil>"` to make them explicit.
395
-- @param ... the parameters to dump to stdout.
396
-- @usage
397
-- -- example debug output
398
-- require"pl.pretty" ("hello", nil, "world", { bye = "world", true} )
399
--
400
-- -- output:
401
-- {
402
--   ["arg 1"] = "hello",
403
--   ["arg 2"] = "<nil>",
404
--   ["arg 3"] = "world",
405
--   ["arg 4"] = {
406
--     true,
407
--     bye = "world"
408
--   }
409
-- }
410
function pretty.debug(...)
210✔
411
    local n = select("#", ...)
×
412
    local t = { ... }
×
413
    for i = 1, n do
×
414
        local value = t[i]
×
415
        if value == nil then
×
416
            value = "<nil>"
×
417
        end
418
        t[i] = nil
×
419
        t["arg " .. i] = value
×
420
    end
421

422
    print(pretty.write(t))
×
423
    return true
×
424
end
425

426

427
local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
210✔
428

429
local function comma (val)
430
    local thou = math.floor(val/1000)
78✔
431
    if thou > 0 then return comma(thou)..','.. tostring(val % 1000)
106✔
432
    else return tostring(val) end
36✔
433
end
434

435
--- Format large numbers nicely for human consumption.
436
-- @number num a number.
437
-- @string[opt] kind one of `'M'` (memory in `KiB`, `MiB`, etc.),
438
-- `'N'` (postfixes are `'K'`, `'M'` and `'B'`),
439
-- or `'T'` (use commas as thousands separator), `'N'` by default.
440
-- @int[opt] prec number of digits to use for `'M'` and `'N'`, `1` by default.
441
function pretty.number (num,kind,prec)
210✔
442
    local fmt = '%.'..(prec or 1)..'f%s'
96✔
443
    if kind == 'T' then
96✔
444
        return comma(num)
36✔
445
    else
446
        local postfixes, fact
447
        if kind == 'M' then
60✔
448
            fact = 1024
30✔
449
            postfixes = memp
30✔
450
        else
451
            fact = 1000
30✔
452
            postfixes = nump
30✔
453
        end
454
        local div = fact
60✔
455
        local k = 1
60✔
456
        while num >= div and k <= #postfixes do
144✔
457
            div = div * fact
84✔
458
            k = k + 1
84✔
459
        end
460
        div = div / fact
60✔
461
        if k > #postfixes then k = k - 1; div = div/fact end
60✔
462
        if k > 1 then
60✔
463
            return fmt:format(num/div,postfixes[k] or 'duh')
48✔
464
        else
465
            return num..postfixes[1]
12✔
466
        end
467
    end
468
end
469

470
return setmetatable(pretty, {
350✔
471
    __call = function(self, ...)
472
        return self.debug(...)
×
473
    end
474
})
175✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc