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

TradeSkillMaster / LibTSMClass / 19745365775

27 Nov 2025 06:39PM UTC coverage: 95.851% (-1.9%) from 97.727%
19745365775

push

github

web-flow
Support for extensions (#29)

46 of 56 new or added lines in 1 file covered. (82.14%)

6 existing lines in 1 file now uncovered.

462 of 482 relevant lines covered (95.85%)

21.94 hits per line

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

95.85
/LibTSMClass.lua
1
--- LibTSMClass Library
2
-- Allows for OOP in lua through the implementation of classes. Many features of proper classes are supported including
3
-- inhertiance, polymorphism, and virtual methods.
4
-- @author TradeSkillMaster Team (admin@tradeskillmaster.com)
5
-- @license MIT
6

7
local MINOR_REVISION = 2
1✔
8
local Lib = {} ---@class LibTSMClass
1✔
9
local private = {
1✔
10
        classInfo = {},
1✔
11
        extensionInfo = {},
1✔
12
        instInfo = {},
1✔
13
        constructTbl = nil,
1✔
14
        tempTable = {},
1✔
15
}
16
-- Set the keys as weak so that class extensions and instances of classes can be GC'd (classes are never GC'd)
17
local WEAK_KEY_MT = { __mode = "k" }
1✔
18
setmetatable(private.extensionInfo, WEAK_KEY_MT)
1✔
19
setmetatable(private.instInfo, WEAK_KEY_MT)
1✔
20
local SPECIAL_PROPERTIES = {
1✔
21
        __init = true,
1✔
22
        __tostring = true,
1✔
23
        __dump = true,
1✔
24
}
25
local RESERVED_KEYS = {
1✔
26
        __super = true,
1✔
27
        __isa = true,
1✔
28
        __class = true,
1✔
29
        __name = true,
1✔
30
        __as = true,
1✔
31
        __static = true,
1✔
32
        __private = true,
1✔
33
        __protected = true,
1✔
34
        __abstract = true,
1✔
35
        __closure = true,
1✔
36
        __extend = true,
1✔
37
}
38
local DEFAULT_INST_FIELDS = {
1✔
39
        __init = function(self)
40
                -- Do nothing
41
        end,
42
        __tostring = function(self)
43
                return private.instInfo[self].str
1✔
44
        end,
45
        __dump = function(self)
46
                private.InstDump(self)
1✔
47
        end,
48
}
49
local DUMP_KEY_PATH_DELIM = "\001"
1✔
50

51

52

53
-- ============================================================================
54
-- Public Library Functions
55
-- ============================================================================
56

57
---@class Class
58

59
---@alias ClassProperties
60
---|'"ABSTRACT"' # An abstract class cannot be directly instantiated
61

62
---Defines a new class.
63
---@generic T: Class
64
---@param name `T` The name of the class
65
---@param superclass? any The superclass
66
---@param ... ClassProperties Properties to define the class with
67
---@return T
68
function Lib.DefineClass(name, superclass, ...)
1✔
69
        if type(name) ~= "string" then
34✔
70
                error("Invalid class name: "..tostring(name), 2)
1✔
71
        end
72
        if superclass ~= nil and (type(superclass) ~= "table" or not private.classInfo[superclass]) then
33✔
73
                error("Invalid superclass: "..tostring(superclass), 2)
1✔
74
        end
75
        local abstract = false
32✔
76
        for i = 1, select('#', ...) do
36✔
77
                local modifier = select(i, ...)
5✔
78
                if modifier == "ABSTRACT" then
5✔
79
                        abstract = true
4✔
80
                else
81
                        error("Invalid modifier: "..tostring(modifier), 2)
1✔
82
                end
83
        end
84

85
        local class = setmetatable({}, private.CLASS_MT)
31✔
86
        private.classInfo[class] = {
31✔
87
                name = name,
31✔
88
                static = {},
31✔
89
                superStatic = {},
31✔
90
                superclass = superclass,
31✔
91
                abstract = abstract,
31✔
92
                referenceType = nil,
31✔
93
                subclassed = false,
31✔
94
                extended = false,
31✔
95
                methodProperties = nil, -- Set as needed
31✔
96
                inClassFunc = 0,
31✔
97
                instances = setmetatable({}, WEAK_KEY_MT),
31✔
98
        }
31✔
99
        while superclass do
50✔
100
                local superclassInfo = private.classInfo[superclass]
19✔
101
                for key, value in pairs(superclassInfo.static) do
83✔
102
                        if not private.classInfo[class].superStatic[key] then
64✔
103
                                private.classInfo[class].superStatic[key] = {
61✔
104
                                        class = superclass,
61✔
105
                                        value = value,
61✔
106
                                        properties = superclassInfo.methodProperties and superclassInfo.methodProperties[key] or nil,
61✔
107
                                }
61✔
108
                        end
109
                end
110
                if superclassInfo.extended then
19✔
NEW
111
                        error("Cannot subclass a class after it's extended", 2)
×
112
                end
113
                superclassInfo.subclassed = true
19✔
114
                superclass = superclass.__super
19✔
115
        end
116
        return class
31✔
117
end
118

119
---Constructs a class from an existing table, preserving its keys.
120
---@generic T
121
---@param tbl table The table with existing keys to preserve
122
---@param class T The class to construct
123
---@param ... any Arguments to pass to the constructor
124
---@return T
125
function Lib.ConstructWithTable(tbl, class, ...)
1✔
126
        private.constructTbl = tbl
1✔
127
        local inst = class(...)
1✔
128
        assert(not private.constructTbl and inst == tbl, "Internal error")
1✔
129
        return inst
1✔
130
end
131

132
---Gets instance properties from an instance string for debugging purposes.
133
---@param instStr string The string representation of the instance
134
---@param maxDepth number The maximum depth to recurse into tables
135
---@param tableLookupFunc? fun(tbl: table): string? A lookup function which is used to get debug information for an unknown table
136
---@return string? @The properties dumped as a multiline string
137
function Lib.GetDebugInfo(instStr, maxDepth, tableLookupFunc)
1✔
138
        local inst = nil
139
        for obj, info in pairs(private.instInfo) do
4✔
140
                if info.str == instStr then
3✔
141
                        inst = obj
1✔
142
                        break
1✔
143
                end
144
        end
145
        if not inst then
2✔
146
                return nil
1✔
147
        end
148
        assert(not next(private.tempTable))
1✔
149
        private.InstDump(inst, private.tempTable, maxDepth, tableLookupFunc)
1✔
150
        local result = table.concat(private.tempTable, "\n")
1✔
151
        wipe(private.tempTable)
1✔
152
        return result
1✔
153
end
154

155

156

157
-- ============================================================================
158
-- Instance Metatable
159
-- ============================================================================
160

161
private.INST_MT = {
1✔
162
        __newindex = function(self, key, value)
163
                if RESERVED_KEYS[key] then
38✔
164
                        error("Can't set reserved key: "..tostring(key), 2)
1✔
165
                end
166
                if private.classInfo[self.__class].static[key] ~= nil then
37✔
167
                        private.classInfo[self.__class].static[key] = value
1✔
168
                elseif not private.instInfo[self].hasSuperclass then
36✔
169
                        -- We just set this directly on the instance table for better performance
170
                        rawset(self, key, value)
16✔
171
                else
172
                        private.instInfo[self].fields[key] = value
20✔
173
                end
174
        end,
175
        __index = function(self, key)
176
                -- This method is super optimized since it's used for every class instance access, meaning function calls and
177
                -- table lookup is kept to an absolute minimum, at the expense of readability and code reuse.
178
                local instInfo = private.instInfo[self]
239✔
179

180
                -- Check if this key is an instance field first, since this is the most common case
181
                local res = instInfo.fields[key]
239✔
182
                if res ~= nil then
239✔
183
                        instInfo.currentClass = nil
56✔
184
                        return res
56✔
185
                end
186

187
                -- Check if it's a special field / method
188
                if key == "__super" then
183✔
189
                        if not instInfo.hasSuperclass then
27✔
190
                                error("The class of this instance has no superclass", 2)
2✔
191
                        end
192
                        -- The class of the current class method we are in, or nil if we're not in a class method.
193
                        local methodClass = instInfo.methodClass
25✔
194
                        -- We can only access the superclass within a class method and will use the class which defined that method
195
                        -- as the base class to jump to the superclass of, regardless of what class the instance actually is.
196
                        if not methodClass then
25✔
197
                                error("The superclass can only be referenced within a class method", 2)
1✔
198
                        end
199
                        return private.InstAs(self, private.classInfo[instInfo.currentClass or methodClass].superclass)
24✔
200
                elseif key == "__as" then
156✔
201
                        return private.InstAs
17✔
202
                elseif key == "__closure" then
139✔
203
                        return private.InstClosure
11✔
204
                end
205

206
                -- Reset the current class since we're not continuing the __super chain
207
                local class = instInfo.currentClass or instInfo.class
128✔
208
                instInfo.currentClass = nil
128✔
209

210
                -- Check if this is a static key
211
                local classInfo = private.classInfo[class]
128✔
212
                res = classInfo.static[key]
128✔
213
                if res ~= nil then
128✔
214
                        return res
77✔
215
                end
216

217
                -- Check if it's a static field in the superclass
218
                local superStaticRes = classInfo.superStatic[key]
51✔
219
                if superStaticRes then
51✔
220
                        res = superStaticRes.value
31✔
221
                        return res
31✔
222
                end
223

224
                -- Check if this field has a default value
225
                res = DEFAULT_INST_FIELDS[key]
20✔
226
                if res ~= nil then
20✔
227
                        return res
15✔
228
                end
229

230
                return nil
5✔
231
        end,
232
        __tostring = function(self)
233
                return self:__tostring()
4✔
234
        end,
235
        __metatable = false,
1✔
236
}
1✔
237

238

239

240
-- ============================================================================
241
-- Class Metatable
242
-- ============================================================================
243

244
private.CLASS_MT = {
1✔
245
        __newindex = function(self, key, value)
246
                if type(key) ~= "string" then
102✔
247
                        error("Can't index class with non-string key", 2)
1✔
248
                end
249
                local classInfo = private.classInfo[self]
101✔
250
                if classInfo.subclassed or classInfo.extended then
101✔
251
                        error("Can't modify classes after they are subclassed or extended", 2)
1✔
252
                end
253
                if classInfo.static[key] then
100✔
254
                        error("Can't modify or override static members", 2)
1✔
255
                end
256
                if RESERVED_KEYS[key] then
99✔
257
                        error("Reserved word: "..key, 2)
1✔
258
                end
259
                local isFunction = type(value) == "function"
98✔
260
                local methodProperty = classInfo.referenceType
98✔
261
                local isStatic = methodProperty == "STATIC"
98✔
262
                classInfo.referenceType = nil
98✔
263
                if isFunction and isStatic then
98✔
264
                        -- We are defining a static class function
265
                        classInfo.methodProperties = classInfo.methodProperties or {}
7✔
266
                        classInfo.methodProperties[key] = "STATIC"
7✔
267
                        -- We wrap static methods so that we can allow private or protected access within them
268
                        classInfo.static[key] = function(...)
7✔
269
                                classInfo.inClassFunc = classInfo.inClassFunc + 1
7✔
270
                                return private.StaticFuncReturnHelper(classInfo, value(...))
7✔
271
                        end
272
                elseif isFunction and not isStatic then
91✔
273
                        -- We are defining a class method
274
                        local superclass = classInfo.superclass
86✔
275
                        while superclass do
118✔
276
                                local superclassInfo = private.classInfo[superclass]
57✔
277
                                local superclassMethodProperty = superclassInfo.methodProperties and superclassInfo.methodProperties[key] or nil
57✔
278
                                if superclassInfo.static[key] ~= nil or superclassMethodProperty ~= nil then
57✔
279
                                        if superclassInfo.static[key] ~= nil and type(superclassInfo.static[key]) ~= "function" then
25✔
280
                                                error(format("Attempting to override non-method superclass property (%s) with method", key), 2)
1✔
281
                                        end
282
                                        if superclassMethodProperty == nil then
24✔
283
                                                -- Can only override public methods with public methods
284
                                                if methodProperty ~= nil then
11✔
285
                                                        error(format("Overriding a public superclass method (%s) can only be done with a public method", key), 2)
1✔
286
                                                end
287
                                        elseif superclassMethodProperty == "ABSTRACT" then
13✔
288
                                                -- Can only override abstract methods with protected methods
289
                                                if methodProperty ~= "PROTECTED" then
5✔
290
                                                        error(format("Overriding an abstract superclass method (%s) can only be done with a protected method", key), 2)
1✔
291
                                                end
292
                                        elseif superclassMethodProperty == "PROTECTED" then
8✔
293
                                                -- Can only override protected methods with protected methods
294
                                                if methodProperty ~= "PROTECTED" then
5✔
295
                                                        error(format("Overriding a protected superclass method (%s) can only be done with a protected method", key), 2)
2✔
296
                                                end
297
                                        elseif superclassMethodProperty == "STATIC" then
3✔
298
                                                -- Can't override static properties with methods
299
                                                error(format("Can't override static superclass property (%s) with method", key), 2)
1✔
300
                                        elseif superclassMethodProperty == "PRIVATE" then
2✔
301
                                                -- Can't override private methods
302
                                                error(format("Can't override private superclass method (%s)", key), 2)
2✔
303
                                        else
304
                                                -- luacov: disable
305
                                                -- Should never get here
306
                                                error("Unexpected superclassMethodProperty: "..tostring(superclassMethodProperty))
307
                                                -- luacov: enable
308
                                        end
309
                                        -- Just need to go up the superclass tree until we find the first one which references this key
310
                                        break
311
                                end
312
                                superclass = superclassInfo.superclass
32✔
313
                        end
314
                        local isPrivate, isProtected = false, false
78✔
315
                        if methodProperty ~= nil then
78✔
316
                                classInfo.methodProperties = classInfo.methodProperties or {}
22✔
317
                                classInfo.methodProperties[key] = methodProperty
22✔
318
                                if methodProperty == "PRIVATE" then
22✔
319
                                        isPrivate = true
7✔
320
                                elseif methodProperty == "PROTECTED" then
15✔
321
                                        isProtected = true
12✔
322
                                elseif methodProperty == "ABSTRACT" then
3✔
323
                                        -- Just need to set the property
324
                                        return
3✔
325
                                else
326
                                        -- luacov: disable
327
                                        -- Should never get here
328
                                        error("Unknown method property: "..tostring(methodProperty))
329
                                        -- luacov: enable
330
                                end
331
                        end
332
                        -- We wrap class methods so that within them, the instance appears to be of the defining class
333
                        classInfo.static[key] = function(inst, ...)
75✔
334
                                local instInfo = private.instInfo[inst]
130✔
335
                                if not instInfo or not instInfo.isClassLookup[self] then
130✔
336
                                        error(format("Attempt to call class method on non-object (%s)", tostring(inst)), 2)
1✔
337
                                end
338
                                local prevMethodClass = instInfo.methodClass
129✔
339
                                if isPrivate and prevMethodClass ~= self and (prevMethodClass ~= nil or classInfo.inClassFunc == 0) then
129✔
340
                                        error(format("Attempting to call private method (%s) from outside of class", key), 2)
6✔
341
                                end
342
                                if isProtected and prevMethodClass == nil and classInfo.inClassFunc == 0 then
123✔
343
                                        error(format("Attempting to call protected method (%s) from outside of class", key), 2)
5✔
344
                                end
345
                                instInfo.methodClass = self
118✔
346
                                classInfo.inClassFunc = classInfo.inClassFunc + 1
118✔
347
                                return private.InstMethodReturnHelper(prevMethodClass, instInfo, classInfo, value(inst, ...))
118✔
348
                        end
349
                elseif not isFunction then
5✔
350
                        -- We are defining a static property (shouldn't be explicitly marked as static)
351
                        if isStatic then
5✔
352
                                error("Unnecessary __static for non-function class property", 2)
1✔
353
                        end
354
                        classInfo.static[key] = value
4✔
355
                end
356
        end,
357
        __index = function(self, key)
358
                local classInfo = private.classInfo[self]
68✔
359
                if classInfo.referenceType ~= nil then
68✔
360
                        error("Can't index into property table", 2)
1✔
361
                end
362
                if key == "__isa" then
67✔
363
                        return private.ClassIsA
3✔
364
                elseif key == "__name" then
64✔
365
                        return classInfo.name
1✔
366
                elseif key == "__super" then
63✔
367
                        return classInfo.superclass
20✔
368
                elseif key == "__static" then
43✔
369
                        classInfo.referenceType = "STATIC"
8✔
370
                        return self
8✔
371
                elseif key == "__private" then
35✔
372
                        classInfo.referenceType = "PRIVATE"
8✔
373
                        return self
8✔
374
                elseif key == "__protected" then
27✔
375
                        classInfo.referenceType = "PROTECTED"
13✔
376
                        return self
13✔
377
                elseif key == "__abstract" then
14✔
378
                        if not classInfo.abstract then
4✔
379
                                error("Can only define abstract methods on abstract classes", 2)
1✔
380
                        end
381
                        classInfo.referenceType = "ABSTRACT"
3✔
382
                        return self
3✔
383
                elseif key == "__extend" then
10✔
384
                        return private.ClassExtend
1✔
385
                elseif classInfo.static[key] ~= nil then
9✔
386
                        return classInfo.static[key]
7✔
387
                elseif classInfo.superStatic[key] then
2✔
388
                        return classInfo.superStatic[key].value
1✔
389
                end
390
                error(format("Invalid static class key (%s)", tostring(key)), 2)
1✔
391
        end,
392
        __tostring = function(self)
393
                return "class:"..private.classInfo[self].name
11✔
394
        end,
395
        __call = function(self, ...)
396
                if private.classInfo[self].abstract then
34✔
397
                        error("Attempting to instantiate an abstract class", 2)
1✔
398
                end
399
                -- Create a new instance of this class
400
                local inst = private.constructTbl or {}
33✔
401
                local instStr = strmatch(tostring(inst), "table:[^1-9a-fA-F]*([0-9a-fA-F]+)")
33✔
402
                setmetatable(inst, private.INST_MT)
33✔
403
                local classInfo = private.classInfo[self]
33✔
404
                local hasSuperclass = classInfo.superclass and true or false
33✔
405
                private.instInfo[inst] = {
33✔
406
                        class = self,
33✔
407
                        fields = {
33✔
408
                                __class = self,
33✔
409
                                __isa = private.InstIsA,
33✔
410
                        },
33✔
411
                        str = classInfo.name..":"..instStr,
33✔
412
                        isClassLookup = {},
33✔
413
                        hasSuperclass = hasSuperclass,
33✔
414
                        currentClass = nil,
33✔
415
                        closures = {},
33✔
416
                }
33✔
417
                classInfo.instances[inst] = true
33✔
418
                if not hasSuperclass then
33✔
419
                        -- Set the static members directly on this object for better performance
420
                        for key, value in pairs(classInfo.static) do
49✔
421
                                if not SPECIAL_PROPERTIES[key] then
34✔
422
                                        rawset(inst, key, value)
26✔
423
                                end
424
                        end
425
                end
426
                -- Check that all the abstract methods have been defined
427
                assert(not next(private.tempTable))
33✔
428
                local c = self
33✔
429
                while c do
89✔
430
                        private.instInfo[inst].isClassLookup[c] = true
56✔
431
                        c = private.classInfo[c].superclass
56✔
432
                        if c and private.classInfo[c] and private.classInfo[c].methodProperties then
56✔
433
                                for methodName, property in pairs(private.classInfo[c].methodProperties) do
47✔
434
                                        if property == "ABSTRACT" then
31✔
435
                                                private.tempTable[methodName] = true
9✔
436
                                        end
437
                                end
438
                        end
439
                end
440
                for methodName in pairs(private.tempTable) do
40✔
441
                        if type(classInfo.static[methodName]) ~= "function" then
9✔
442
                                -- Check the superclasses
443
                                local found = false
4✔
444
                                local c2 = self
4✔
445
                                while c2 and not found do
11✔
446
                                        c2 = private.classInfo[c2].superclass
7✔
447
                                        if c2 and private.classInfo[c2] and private.classInfo[c2].static[methodName] then
7✔
448
                                                found = true
2✔
449
                                        end
450
                                end
451
                                if not found then
4✔
452
                                        wipe(private.tempTable)
2✔
453
                                        error("Missing abstract method: "..tostring(methodName), 2)
2✔
454
                                end
455
                        end
456
                end
457
                wipe(private.tempTable)
31✔
458
                if private.constructTbl then
31✔
459
                        -- Re-set all the object attributes through the proper metamethod
460
                        assert(not next(private.tempTable))
1✔
461
                        for k, v in pairs(inst) do
4✔
462
                                private.tempTable[k] = v
3✔
463
                        end
464
                        for k, v in pairs(private.tempTable) do
4✔
465
                                rawset(inst, k, nil)
3✔
466
                                inst[k] = v
3✔
467
                        end
468
                        wipe(private.tempTable)
1✔
469
                        private.constructTbl = nil
1✔
470
                end
471
                if select("#", inst:__init(...)) > 0 then
31✔
472
                        error("__init(...) must not return any values", 2)
1✔
473
                end
474
                return inst
26✔
475
        end,
476
        __metatable = false,
1✔
477
}
1✔
478

479

480

481
-- ============================================================================
482
-- Extension Metatable
483
-- ============================================================================
484

485
private.EXTENSION_MT = {
1✔
486
        __newindex = function(self, key, value)
487
                if type(key) ~= "string" then
2✔
NEW
488
                        error("Can't index class extension with non-string key", 2)
×
489
                elseif type(value) ~= "function" then
2✔
NEW
490
                        error("Can only add class methods via class extension", 2)
×
491
                elseif RESERVED_KEYS[key] then
2✔
NEW
492
                        error("Reserved word: "..key, 2)
×
493
                end
494
                local class = private.extensionInfo[self].class
2✔
495
                local classInfo = private.classInfo[class]
2✔
496
                if classInfo.subclassed then
2✔
NEW
497
                        error("Can't add extension methods after a class is subclassed", 2)
×
498
                end
499
                local testClass = class
2✔
500
                while testClass do
4✔
501
                        local testClassInfo = private.classInfo[testClass]
2✔
502
                        if testClassInfo.static[key] ~= nil then
2✔
NEW
503
                                error("Can't modify or override class members from extension", 2)
×
504
                        end
505
                        testClass = testClassInfo.superclass
2✔
506
                end
507
                -- Don't need to wrap extension methods because they are treated as if they are outside
508
                -- the class as far as access restrictions are concerned and don't need to worry about
509
                -- virtual methods since extensions only support non-subclassed classes.
510
                classInfo.static[key] = value
2✔
511
                -- Add the new method directly to all previously-created instances
512
                for inst in pairs(classInfo.instances) do
5✔
513
                        rawset(inst, key, value)
3✔
514
                end
515
        end,
516
        __index = function()
NEW
517
                error("Extension objects are write-only", 2)
×
518
        end,
519
        __tostring = function(self)
NEW
520
                return "classExtension:"..private.classInfo[private.extensionInfo[self].class].name
×
521
        end,
522
        __metatable = false,
1✔
523
}
1✔
524

525

526

527
-- ============================================================================
528
-- Helper Functions
529
-- ============================================================================
530

531
function private.ClassIsA(class, targetClass)
1✔
532
        while class do
4✔
533
                if class == targetClass then return true end
4✔
534
                class = class.__super
1✔
535
        end
536
end
537

538
function private.ClassExtend(class)
1✔
539
        local classInfo = private.classInfo[class]
1✔
540
        if not classInfo then
1✔
NEW
541
                error("__extend() must be called with `:` to pass the class", 2)
×
542
        elseif classInfo.subclassed then
1✔
NEW
543
                error("Can't add extension methods after a class is subclassed", 2)
×
544
        end
545
        classInfo.extended = true
1✔
546
        local extensionObj = setmetatable({}, private.EXTENSION_MT)
1✔
547
        private.extensionInfo[extensionObj] = {
1✔
548
                class = class,
1✔
549
        }
1✔
550
        return extensionObj
1✔
551
end
552

553
function private.InstMethodReturnHelper(class, instInfo, classInfo, ...)
1✔
554
        -- Reset methodClass and decrement inClassFunc now that the function returned
555
        instInfo.methodClass = class
102✔
556
        classInfo.inClassFunc = classInfo.inClassFunc - 1
102✔
557
        return ...
102✔
558
end
559

560
function private.StaticFuncReturnHelper(classInfo, ...)
1✔
561
        -- Decrement inClassFunc now that the function returned
562
        classInfo.inClassFunc = classInfo.inClassFunc - 1
7✔
563
        return ...
7✔
564
end
565

566
function private.InstIsA(inst, targetClass)
1✔
567
        return private.instInfo[inst].isClassLookup[targetClass]
2✔
568
end
569

570
function private.InstAs(inst, targetClass)
1✔
571
        local instInfo = private.instInfo[inst]
41✔
572
        -- Clear currentClass while we perform our checks so we can better recover from errors
573
        instInfo.currentClass = nil
41✔
574
        if not targetClass then
41✔
575
                error(format("Requested class does not exist"), 2)
4✔
576
        elseif not instInfo.isClassLookup[targetClass] then
37✔
577
                error(format("Object is not an instance of the requested class (%s)", tostring(targetClass)), 2)
3✔
578
        end
579
        -- For classes with no superclass, we don't go through the __index metamethod, so can't use __as
580
        if not instInfo.hasSuperclass then
34✔
581
                error("The class of this instance has no superclass", 2)
1✔
582
        end
583
        -- We can only access the superclass within a class method.
584
        if not instInfo.methodClass then
33✔
585
                error("The superclass can only be referenced within a class method", 2)
1✔
586
        end
587
        instInfo.currentClass = targetClass
32✔
588
        return inst
32✔
589
end
590

591
function private.InstClosure(inst, methodName)
1✔
592
        local instInfo = private.instInfo[inst]
11✔
593
        local methodClass = instInfo.methodClass
11✔
594
        if not methodClass then
11✔
595
                error("Closures can only be created within a class method", 2)
1✔
596
        elseif instInfo.currentClass then
10✔
597
                error("Cannot create closure as superclass", 2)
1✔
598
        end
599
        -- Check for this method on the class
600
        local classInfo = private.classInfo[instInfo.class]
9✔
601
        local methodFunc = classInfo.static[methodName]
9✔
602
        if methodFunc then
9✔
603
                -- If this method is private, make sure we're within the class
604
                if classInfo.methodProperties and classInfo.methodProperties[methodName] == "PRIVATE" and instInfo.class ~= methodClass then
5✔
605
                        error("Attempt to create closure for private virtual method", 2)
1✔
606
                end
607
        else
608
                -- Check for this method on the superclass
609
                local superInfo = classInfo.superStatic[methodName]
4✔
610
                if superInfo then
4✔
611
                        if superInfo.properties == "PRIVATE" and not private.classInfo[methodClass].static[methodName] then
3✔
612
                                error("Attempt to create closure for private superclass method", 2)
1✔
613
                        end
614
                        methodFunc = superInfo.value
2✔
615
                end
616
        end
617
        if type(methodFunc) ~= "function" then
7✔
618
                error("Attempt to create closure for non-method field", 2)
1✔
619
        end
620
        local methodClassInfo = private.classInfo[methodClass]
6✔
621
        local cacheKey = tostring(methodClass).."."..methodName
6✔
622
        if not instInfo.closures[cacheKey] then
6✔
623
                instInfo.closures[cacheKey] = function(...)
6✔
624
                        if instInfo.methodClass == methodClass then
8✔
625
                                -- We're already within a method of the class, so just call the method normally
626
                                return methodFunc(inst, ...)
5✔
627
                        else
628
                                -- Pretend we are within the class which created the closure
629
                                local prevClass = instInfo.methodClass
3✔
630
                                instInfo.methodClass = methodClass
3✔
631
                                methodClassInfo.inClassFunc = methodClassInfo.inClassFunc + 1
3✔
632
                                return private.InstMethodReturnHelper(prevClass, instInfo, methodClassInfo, methodFunc(inst, ...))
3✔
633
                        end
634
                end
635
        end
636
        return instInfo.closures[cacheKey]
6✔
637
end
638

639
function private.InstDump(inst, resultTbl, maxDepth, tableLookupFunc)
1✔
640
        local context = {
2✔
641
                resultTbl = resultTbl,
2✔
642
                maxDepth = maxDepth or 2,
2✔
643
                maxTableEntries = 100,
2✔
644
                tableLookupFunc = tableLookupFunc,
2✔
645
                tableRefs = {},
2✔
646
                depth = 0,
2✔
647
        }
648

649
        -- Build up our table references via a breadth-first search
650
        local bfsQueueKeyPath = {""}
2✔
651
        local bfsQueueDepth = {0}
2✔
652
        local bfsQueueValue = {inst}
2✔
653
        while #bfsQueueKeyPath > 0 do
18✔
654
                local keyPath = tremove(bfsQueueKeyPath, 1)
16✔
655
                local depth = tremove(bfsQueueDepth, 1)
16✔
656
                local value = tremove(bfsQueueValue, 1)
16✔
657
                if not context.tableRefs[value] then
16✔
658
                        context.tableRefs[value] = keyPath
14✔
659
                        if depth <= context.maxDepth and not private.classInfo[value] then
14✔
660
                                if private.instInfo[value] then
10✔
661
                                        local instInfo = private.instInfo[value]
2✔
662
                                        value = instInfo.hasSuperclass and instInfo.fields or value
2✔
663
                                end
664
                                for k, v in pairs(value) do
36✔
665
                                        if type(v) == "table" and (type(k) == "string" or type(k) == "number") and not strfind(k, DUMP_KEY_PATH_DELIM, nil, true) then
26✔
666
                                                tinsert(bfsQueueKeyPath, keyPath..DUMP_KEY_PATH_DELIM..k)
14✔
667
                                                tinsert(bfsQueueDepth, depth + 1)
14✔
668
                                                tinsert(bfsQueueValue, v)
14✔
669
                                        end
670
                                end
671
                        end
672
                end
673
        end
674

675
        private.InstDumpVariable("self", inst, context, "")
2✔
676
end
677

678
function private.InstDumpVariable(key, value, context, strKeyPath)
1✔
679
        if strfind(key, DUMP_KEY_PATH_DELIM, nil, true) then
28✔
680
                -- Ignore keys with our deliminator in them
UNCOV
681
                return
×
682
        end
683
        if type(value) == "table" and private.classInfo[value] then
28✔
684
                -- This is a class
685
                private.InstDumpKeyValue(key, "\""..tostring(value).."\"", context)
2✔
686
        elseif type(value) == "table" then
26✔
687
                local refKeyPath = context.tableRefs[value]
14✔
688
                if not refKeyPath then
14✔
UNCOV
689
                        return
×
690
                end
691
                if refKeyPath ~= strKeyPath then
14✔
692
                        local refValue = "\"REF{"..gsub(refKeyPath, DUMP_KEY_PATH_DELIM, ".").."}\""
2✔
693
                        private.InstDumpKeyValue(key, refValue, context)
2✔
694
                elseif private.instInfo[value] then
12✔
695
                        -- This is an instance of a class
696
                        if context.depth <= context.maxDepth then
2✔
697
                                -- Recurse into the class
698
                                local instInfo = private.instInfo[value]
2✔
699
                                local tbl = instInfo.hasSuperclass and instInfo.fields or value
2✔
700
                                private.InstDumpLine(key.." = <"..instInfo.str.."> {", context)
2✔
701
                                context.depth = context.depth + 1
2✔
702
                                for key2, value2 in pairs(tbl) do
14✔
703
                                        if type(key2) == "string" or type(key2) == "number" or type(key2) == "boolean" then
12✔
704
                                                private.InstDumpVariable(key2, value2, context, strKeyPath..DUMP_KEY_PATH_DELIM..key2)
12✔
705
                                        end
706
                                end
707
                                context.depth = context.depth - 1
2✔
708
                                private.InstDumpLine("}", context)
2✔
709
                        else
UNCOV
710
                                private.InstDumpKeyValue(key, "\""..tostring(value).."\"", context)
×
711
                        end
712
                else
713
                        local isEmpty = true
10✔
714
                        for _, value2 in pairs(value) do
10✔
715
                                local valueType = type(value2)
8✔
716
                                if valueType == "string" or valueType == "number" or valueType == "boolean" or valueType == "table" then
8✔
717
                                        isEmpty = false
8✔
718
                                        break
8✔
719
                                end
720
                        end
721
                        if isEmpty then
10✔
722
                                local info = context.tableLookupFunc and context.tableLookupFunc(value) or nil
2✔
723
                                if info and context.depth <= context.maxDepth then
2✔
724
                                        -- Display the table values
UNCOV
725
                                        private.InstDumpKeyValue(key, "{", context)
×
726
                                        context.depth = context.depth + 1
×
727
                                        for _, line in ipairs({strsplit("\n", info)}) do
×
728
                                                private.InstDumpLine(line, context)
×
729
                                        end
UNCOV
730
                                        context.depth = context.depth - 1
×
731
                                        private.InstDumpLine("}", context)
×
732
                                elseif info then
2✔
UNCOV
733
                                        private.InstDumpKeyValue(key, "{ ... }", context)
×
734
                                else
735
                                        private.InstDumpKeyValue(key, "{}", context)
2✔
736
                                end
737
                        else
738
                                if context.depth <= context.maxDepth then
8✔
739
                                        -- Recurse into the table
740
                                        private.InstDumpKeyValue(key, "{", context)
6✔
741
                                        context.depth = context.depth + 1
6✔
742
                                        local numTableEntries = 0
6✔
743
                                        for key2, value2 in pairs(value) do
20✔
744
                                                if numTableEntries >= context.maxTableEntries then
14✔
745
                                                        break
746
                                                end
747
                                                if type(key2) == "string" or type(key2) == "number" or type(key2) == "boolean" then
14✔
748
                                                        numTableEntries = numTableEntries + 1
14✔
749
                                                        private.InstDumpVariable(key2, value2, context, strKeyPath..DUMP_KEY_PATH_DELIM..key2)
14✔
750
                                                end
751
                                        end
752
                                        context.depth = context.depth - 1
6✔
753
                                        private.InstDumpLine("}", context)
6✔
754
                                else
755
                                        private.InstDumpKeyValue(key, "{ ... }", context)
2✔
756
                                end
757
                        end
758
                end
759
        elseif type(value) == "string" then
12✔
760
                private.InstDumpKeyValue(key, "\""..value.."\"", context)
2✔
761
        elseif type(value) == "number" or type(value) == "boolean" then
10✔
762
                private.InstDumpKeyValue(key, value, context)
10✔
763
        end
764
end
765

766
function private.InstDumpLine(line, context)
1✔
767
        line = strrep("  ", context.depth)..line
36✔
768
        if context.resultTbl then
36✔
769
                tinsert(context.resultTbl, line)
18✔
770
        else
771
                print(line)
18✔
772
        end
773
end
774

775
function private.InstDumpKeyValue(key, value, context)
1✔
776
        key = tostring(key)
26✔
777
        if key == "" then
26✔
778
                key = "\"\""
2✔
779
        end
780
        if not context.resultTbl then
26✔
781
                key = "|cff88ccff"..key.."|r"
13✔
782
        end
783
        value = tostring(value)
26✔
784
        local line = format("%s = %s", key, value)
26✔
785
        private.InstDumpLine(line, context)
26✔
786
end
787

788

789

790
-- ============================================================================
791
-- Initialization Code
792
-- ============================================================================
793

794
do
795
        -- Register with LibStub
796
        local libStubTbl = LibStub:NewLibrary("LibTSMClass", MINOR_REVISION)
1✔
797
        if libStubTbl then
1✔
798
                for k, v in pairs(Lib) do
4✔
799
                        libStubTbl[k] = v
3✔
800
                end
801
        end
802
        -- Return the library and our private table for unit testing
803
        return {Lib, private}
1✔
804
end
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

© 2026 Coveralls, Inc