inspect.lua (9429B)
1 --- @diagnostic disable: no-unknown 2 local inspect = { 3 _VERSION = 'inspect.lua 3.1.0', 4 _URL = 'http://github.com/kikito/inspect.lua', 5 _DESCRIPTION = 'human-readable representations of tables', 6 _LICENSE = [[ 7 MIT LICENSE 8 9 Copyright (c) 2013 Enrique GarcĂa Cota 10 11 Permission is hereby granted, free of charge, to any person obtaining a 12 copy of this software and associated documentation files (the 13 "Software"), to deal in the Software without restriction, including 14 without limitation the rights to use, copy, modify, merge, publish, 15 distribute, sublicense, and/or sell copies of the Software, and to 16 permit persons to whom the Software is furnished to do so, subject to 17 the following conditions: 18 19 The above copyright notice and this permission notice shall be included 20 in all copies or substantial portions of the Software. 21 22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 23 OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 25 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 26 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 27 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 28 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 ]], 30 } 31 32 inspect.KEY = setmetatable({}, { 33 __tostring = function() 34 return 'inspect.KEY' 35 end, 36 }) 37 inspect.METATABLE = setmetatable({}, { 38 __tostring = function() 39 return 'inspect.METATABLE' 40 end, 41 }) 42 43 local tostring = tostring 44 local rep = string.rep 45 local match = string.match 46 local char = string.char 47 local gsub = string.gsub 48 local fmt = string.format 49 50 local sbavailable, stringbuffer = pcall(require, 'string.buffer') 51 local buffnew 52 local puts 53 local render 54 55 if sbavailable then 56 buffnew = stringbuffer.new 57 puts = function(buf, str) 58 buf:put(str) 59 end 60 render = function(buf) 61 return buf:get() 62 end 63 else 64 buffnew = function() 65 return { n = 0 } 66 end 67 puts = function(buf, str) 68 buf.n = buf.n + 1 69 buf[buf.n] = str 70 end 71 render = function(buf) 72 return table.concat(buf) 73 end 74 end 75 76 local _rawget 77 if rawget then 78 _rawget = rawget 79 else 80 _rawget = function(t, k) 81 return t[k] 82 end 83 end 84 local function rawpairs(t) 85 return next, t, nil 86 end 87 88 -- Apostrophizes the string if it has quotes, but not aphostrophes 89 -- Otherwise, it returns a regular quoted string 90 local function smartQuote(str) 91 if match(str, '"') and not match(str, "'") then 92 return "'" .. str .. "'" 93 end 94 return '"' .. gsub(str, '"', '\\"') .. '"' 95 end 96 97 -- \a => '\\a', \0 => '\\0', 31 => '\31' 98 local shortControlCharEscapes = { 99 ['\a'] = '\\a', 100 ['\b'] = '\\b', 101 ['\f'] = '\\f', 102 ['\n'] = '\\n', 103 ['\r'] = '\\r', 104 ['\t'] = '\\t', 105 ['\v'] = '\\v', 106 ['\127'] = '\\127', 107 } 108 local longControlCharEscapes = { ['\127'] = '\127' } 109 for i = 0, 31 do 110 local ch = char(i) 111 if not shortControlCharEscapes[ch] then 112 shortControlCharEscapes[ch] = '\\' .. i 113 longControlCharEscapes[ch] = fmt('\\%03d', i) 114 end 115 end 116 117 local function escape(str) 118 return ( 119 gsub( 120 gsub(gsub(str, '\\', '\\\\'), '(%c)%f[0-9]', longControlCharEscapes), 121 '%c', 122 shortControlCharEscapes 123 ) 124 ) 125 end 126 127 local luaKeywords = {} 128 for k in 129 ([[ and break do else elseif end false for function goto if 130 in local nil not or repeat return then true until while 131 ]]):gmatch('%w+') 132 do 133 luaKeywords[k] = true 134 end 135 136 local function isIdentifier(str) 137 return type(str) == 'string' 138 -- identifier must start with a letter and underscore, and be followed by letters, numbers, and underscores 139 and not not str:match('^[_%a][_%a%d]*$') 140 -- lua keywords are not valid identifiers 141 and not luaKeywords[str] 142 end 143 144 local flr = math.floor 145 local function isSequenceKey(k, sequenceLength) 146 return type(k) == 'number' and flr(k) == k and 1 <= k and k <= sequenceLength 147 end 148 149 local defaultTypeOrders = { 150 ['number'] = 1, 151 ['boolean'] = 2, 152 ['string'] = 3, 153 ['table'] = 4, 154 ['function'] = 5, 155 ['userdata'] = 6, 156 ['thread'] = 7, 157 } 158 159 local function sortKeys(a, b) 160 local ta, tb = type(a), type(b) 161 162 -- strings and numbers are sorted numerically/alphabetically 163 if ta == tb and (ta == 'string' or ta == 'number') then 164 return a < b 165 end 166 167 local dta = defaultTypeOrders[ta] or 100 168 local dtb = defaultTypeOrders[tb] or 100 169 -- Two default types are compared according to the defaultTypeOrders table 170 171 -- custom types are sorted out alphabetically 172 return dta == dtb and ta < tb or dta < dtb 173 end 174 175 local function getKeys(t) 176 local seqLen = 1 177 while _rawget(t, seqLen) ~= nil do 178 seqLen = seqLen + 1 179 end 180 seqLen = seqLen - 1 181 182 local keys, keysLen = {}, 0 183 for k in rawpairs(t) do 184 if not isSequenceKey(k, seqLen) then 185 keysLen = keysLen + 1 186 keys[keysLen] = k 187 end 188 end 189 table.sort(keys, sortKeys) 190 return keys, keysLen, seqLen 191 end 192 193 local function countCycles(x, cycles, depth) 194 if type(x) == 'table' then 195 if cycles[x] then 196 cycles[x] = cycles[x] + 1 197 else 198 cycles[x] = 1 199 if depth > 0 then 200 for k, v in rawpairs(x) do 201 countCycles(k, cycles, depth - 1) 202 countCycles(v, cycles, depth - 1) 203 end 204 countCycles(getmetatable(x), cycles, depth - 1) 205 end 206 end 207 end 208 end 209 210 local function makePath(path, a, b) 211 local newPath = {} 212 local len = #path 213 for i = 1, len do 214 newPath[i] = path[i] 215 end 216 217 newPath[len + 1] = a 218 newPath[len + 2] = b 219 220 return newPath 221 end 222 223 local function processRecursive(process, item, path, visited) 224 if item == nil then 225 return nil 226 end 227 if visited[item] then 228 return visited[item] 229 end 230 231 local processed = process(item, path) 232 if type(processed) == 'table' then 233 local processedCopy = {} 234 visited[item] = processedCopy 235 local processedKey 236 237 for k, v in rawpairs(processed) do 238 processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 239 if processedKey ~= nil then 240 processedCopy[processedKey] = 241 processRecursive(process, v, makePath(path, processedKey), visited) 242 end 243 end 244 245 local mt = 246 processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 247 if type(mt) ~= 'table' then 248 mt = nil 249 end 250 setmetatable(processedCopy, mt) 251 processed = processedCopy 252 end 253 return processed 254 end 255 256 local Inspector = {} 257 258 local Inspector_mt = { __index = Inspector } 259 260 local function tabify(inspector) 261 puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) 262 end 263 264 function Inspector:getId(v) 265 local id = self.ids[v] 266 local ids = self.ids 267 if not id then 268 local tv = type(v) 269 id = (ids[tv] or 0) + 1 270 ids[v], ids[tv] = id, id 271 end 272 return tostring(id) 273 end 274 275 function Inspector:putValue(v) 276 local buf = self.buf 277 local tv = type(v) 278 if tv == 'string' then 279 puts(buf, smartQuote(escape(v))) 280 elseif 281 tv == 'number' 282 or tv == 'boolean' 283 or tv == 'nil' 284 or tv == 'cdata' 285 or tv == 'ctype' 286 or (vim and v == vim.NIL) 287 then 288 puts(buf, tostring(v)) 289 elseif tv == 'table' and not self.ids[v] then 290 local t = v 291 292 if t == inspect.KEY or t == inspect.METATABLE then 293 puts(buf, tostring(t)) 294 elseif self.level >= self.depth then 295 puts(buf, '{...}') 296 else 297 if self.cycles[t] > 1 then 298 puts(buf, fmt('<%d>', self:getId(t))) 299 end 300 301 local keys, keysLen, seqLen = getKeys(t) 302 local mt = getmetatable(t) 303 304 if vim and seqLen == 0 and keysLen == 0 and mt == vim._empty_dict_mt then 305 puts(buf, tostring(t)) 306 return 307 end 308 309 puts(buf, '{') 310 self.level = self.level + 1 311 312 for i = 1, seqLen + keysLen do 313 if i > 1 then 314 puts(buf, ',') 315 end 316 if i <= seqLen then 317 puts(buf, ' ') 318 self:putValue(t[i]) 319 else 320 local k = keys[i - seqLen] 321 tabify(self) 322 if isIdentifier(k) then 323 puts(buf, k) 324 else 325 puts(buf, '[') 326 self:putValue(k) 327 puts(buf, ']') 328 end 329 puts(buf, ' = ') 330 self:putValue(t[k]) 331 end 332 end 333 334 if type(mt) == 'table' then 335 if seqLen + keysLen > 0 then 336 puts(buf, ',') 337 end 338 tabify(self) 339 puts(buf, '<metatable> = ') 340 self:putValue(mt) 341 end 342 343 self.level = self.level - 1 344 345 if keysLen > 0 or type(mt) == 'table' then 346 tabify(self) 347 elseif seqLen > 0 then 348 puts(buf, ' ') 349 end 350 351 puts(buf, '}') 352 end 353 else 354 puts(buf, fmt('<%s %d>', tv, self:getId(v))) 355 end 356 end 357 358 function inspect.inspect(root, options) 359 options = options or {} 360 361 local depth = options.depth or math.huge 362 local newline = options.newline or '\n' 363 local indent = options.indent or ' ' 364 local process = options.process 365 366 if process then 367 root = processRecursive(process, root, {}, {}) 368 end 369 370 local cycles = {} 371 countCycles(root, cycles, depth) 372 373 local inspector = setmetatable({ 374 buf = buffnew(), 375 ids = {}, 376 cycles = cycles, 377 depth = depth, 378 level = 0, 379 newline = newline, 380 indent = indent, 381 }, Inspector_mt) 382 383 inspector:putValue(root) 384 385 return render(inspector.buf) 386 end 387 388 setmetatable(inspect, { 389 __call = function(_, root, options) 390 return inspect.inspect(root, options) 391 end, 392 }) 393 394 return inspect