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

TradeSkillMaster / LibTSMClass / 27472388174

13 Jun 2026 04:26PM UTC coverage: 94.51% (-0.9%) from 95.446%
27472388174

push

github

web-flow
Improve wowlua-ls annotations (#39)

1 of 6 new or added lines in 1 file covered. (16.67%)

19 existing lines in 1 file now uncovered.

482 of 510 relevant lines covered (94.51%)

26.77 hits per line

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

94.51
/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 RESERVED_KEYS = {
1✔
21
        __super = true,
1✔
22
        __isa = true,
1✔
23
        __class = true,
1✔
24
        __name = true,
1✔
25
        __as = true,
1✔
26
        __static = true,
1✔
27
        __private = true,
1✔
28
        __protected = true,
1✔
29
        __abstract = true,
1✔
30
        __closure = true,
1✔
31
        __extend = true,
1✔
32
}
33
local DEFAULT_INST_FIELDS = {
1✔
34
        __init = function(self)
35
                -- Do nothing
36
        end,
37
        __tostring = function(self)
38
                return private.instInfo[self].str
1✔
39
        end,
40
        ToDebugString = function(self)
41
                return private.instInfo[self].str
×
42
        end,
43
        __equals = function(self, other)
44
                return rawequal(self, other)
×
45
        end,
46
        __dump = function(self)
47
                private.InstDump(self)
1✔
48
        end,
49
}
50
local DUMP_KEY_PATH_DELIM = "\001"
1✔
51

52

53

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

58

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

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

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

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

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

157

158

159
-- ============================================================================
160
-- Instance Metatable
161
-- ============================================================================
162

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

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

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

208
                -- Reset the current class since we're not continuing the __super chain
209
                local class = instInfo.currentClass or instInfo.class
131✔
210
                instInfo.currentClass = nil
131✔
211

212
                -- Check if this is a static key
213
                local classInfo = private.classInfo[class]
131✔
214
                res = classInfo.static[key]
131✔
215
                if res ~= nil then
131✔
216
                        return res
72✔
217
                end
218

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

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

232
                return nil
5✔
233
        end,
234
        __eq = function(self, other)
235
                if self.__class ~= other.__class then
5✔
236
                        return false
2✔
237
                end
238
                return self:__equals(other)
3✔
239
        end,
240
        __tostring = function(self)
241
                return self:__tostring()
4✔
242
        end,
243
        __metatable = false,
1✔
244
}
1✔
245

246

247

248
-- ============================================================================
249
-- Class Metatable
250
-- ============================================================================
251

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

499

500

501
-- ============================================================================
502
-- Extension Metatable
503
-- ============================================================================
504

505
private.EXTENSION_MT = {
1✔
506
        __newindex = function(self, key, value)
507
                if type(key) ~= "string" then
2✔
UNCOV
508
                        error("Can't index class extension with non-string key", 2)
×
509
                elseif type(value) ~= "function" then
2✔
UNCOV
510
                        error("Can only add class methods via class extension", 2)
×
511
                elseif RESERVED_KEYS[key] then
2✔
UNCOV
512
                        error("Reserved word: "..key, 2)
×
513
                end
514
                local class = private.extensionInfo[self].class
2✔
515
                local classInfo = private.classInfo[class]
2✔
516
                if classInfo.subclassed then
2✔
UNCOV
517
                        error("Can't add extension methods after a class is subclassed", 2)
×
518
                end
519
                local testClass = class
2✔
520
                while testClass do
4✔
521
                        local testClassInfo = private.classInfo[testClass]
2✔
522
                        if testClassInfo.static[key] ~= nil then
2✔
523
                                error("Can't modify or override class members from extension", 2)
×
524
                        end
525
                        testClass = testClassInfo.superclass
2✔
526
                end
527
                -- Don't need to wrap extension methods because they are treated as if they are outside
528
                -- the class as far as access restrictions are concerned and don't need to worry about
529
                -- virtual methods since extensions only support non-subclassed classes.
530
                classInfo.static[key] = value
2✔
531
                -- Add the new method directly to all previously-created instances
532
                for inst in pairs(classInfo.instances) do
5✔
533
                        rawset(inst, key, value)
3✔
534
                end
535
        end,
536
        __index = function()
UNCOV
537
                error("Extension objects are write-only", 2)
×
538
        end,
539
        __tostring = function(self)
UNCOV
540
                return "classExtension:"..private.classInfo[private.extensionInfo[self].class].name
×
541
        end,
542
        __metatable = false,
1✔
543
}
1✔
544

545

546

547
-- ============================================================================
548
-- Helper Functions
549
-- ============================================================================
550

551
function private.ClassIsA(class, targetClass)
1✔
552
        while class do
4✔
553
                if class == targetClass then return true end
4✔
554
                class = class.__super
1✔
555
        end
556
end
557

558
function private.ClassExtend(class)
1✔
559
        local classInfo = private.classInfo[class]
1✔
560
        if not classInfo then
1✔
UNCOV
561
                error("__extend() must be called with `:` to pass the class", 2)
×
562
        elseif classInfo.subclassed then
1✔
UNCOV
563
                error("Can't add extension methods after a class is subclassed", 2)
×
564
        end
565
        classInfo.extended = true
1✔
566
        local extensionObj = setmetatable({}, private.EXTENSION_MT)
1✔
567
        private.extensionInfo[extensionObj] = {
1✔
568
                class = class,
1✔
569
        }
1✔
570
        return extensionObj
1✔
571
end
572

573
function private.InstMethodReturnHelper(class, instInfo, classInfo, ...)
1✔
574
        -- Reset methodClass and decrement inClassFunc now that the function returned
575
        instInfo.methodClass = class
122✔
576
        while classInfo do
319✔
577
                classInfo.inClassFunc = classInfo.inClassFunc - 1
197✔
578
                local superclass = classInfo.superclass
197✔
579
                classInfo = superclass and private.classInfo[superclass] or nil
197✔
580
        end
581
        return ...
122✔
582
end
583

584
function private.StaticFuncReturnHelper(classInfo, ...)
1✔
585
        -- Decrement inClassFunc now that the function returned
586
        while classInfo do
20✔
587
                classInfo.inClassFunc = classInfo.inClassFunc - 1
12✔
588
                local superclass = classInfo.superclass
12✔
589
                classInfo = superclass and private.classInfo[superclass] or nil
12✔
590
        end
591
        return ...
8✔
592
end
593

594
function private.InstIsA(inst, targetClass)
1✔
595
        return private.instInfo[inst].isClassLookup[targetClass]
2✔
596
end
597

598
function private.InstAs(inst, targetClass)
1✔
599
        local instInfo = private.instInfo[inst]
43✔
600
        -- Clear currentClass while we perform our checks so we can better recover from errors
601
        instInfo.currentClass = nil
43✔
602
        if not targetClass then
43✔
603
                error(format("Requested class does not exist"), 2)
4✔
604
        elseif not instInfo.isClassLookup[targetClass] then
39✔
605
                error(format("Object is not an instance of the requested class (%s)", tostring(targetClass)), 2)
3✔
606
        end
607
        -- For classes with no superclass, we don't go through the __index metamethod, so can't use __as
608
        if not instInfo.hasSuperclass then
36✔
609
                error("The class of this instance has no superclass", 2)
1✔
610
        end
611
        -- We can only access the superclass within a class method.
612
        if not instInfo.methodClass then
35✔
613
                error("The superclass can only be referenced within a class method", 2)
1✔
614
        end
615
        instInfo.currentClass = targetClass
34✔
616
        return inst
34✔
617
end
618

619
function private.InstClosure(inst, methodName)
1✔
620
        local instInfo = private.instInfo[inst]
11✔
621
        local methodClass = instInfo.methodClass
11✔
622
        if not methodClass then
11✔
623
                error("Closures can only be created within a class method", 2)
1✔
624
        elseif instInfo.currentClass then
10✔
625
                error("Cannot create closure as superclass", 2)
1✔
626
        end
627
        -- Check for this method on the class
628
        local classInfo = private.classInfo[instInfo.class]
9✔
629
        local methodFunc = classInfo.static[methodName]
9✔
630
        if methodFunc then
9✔
631
                -- If this method is private, make sure we're within the class
632
                if classInfo.methodProperties and classInfo.methodProperties[methodName] == "PRIVATE" and instInfo.class ~= methodClass then
5✔
633
                        error("Attempt to create closure for private virtual method", 2)
1✔
634
                end
635
        else
636
                -- Check for this method on the superclass
637
                local superInfo = classInfo.superStatic[methodName]
4✔
638
                if superInfo then
4✔
639
                        if superInfo.properties == "PRIVATE" and not private.classInfo[methodClass].static[methodName] then
3✔
640
                                error("Attempt to create closure for private superclass method", 2)
1✔
641
                        end
642
                        methodFunc = superInfo.value
2✔
643
                end
644
        end
645
        if type(methodFunc) ~= "function" then
7✔
646
                error("Attempt to create closure for non-method field", 2)
1✔
647
        end
648
        local methodClassInfo = private.classInfo[methodClass]
6✔
649
        local cacheKey = tostring(methodClass).."."..methodName
6✔
650
        if not instInfo.closures[cacheKey] then
6✔
651
                instInfo.closures[cacheKey] = function(...)
6✔
652
                        if instInfo.methodClass == methodClass then
8✔
653
                                -- We're already within a method of the class, so just call the method normally
654
                                return methodFunc(inst, ...)
5✔
655
                        else
656
                                -- Pretend we are within the class which created the closure
657
                                local prevClass = instInfo.methodClass
3✔
658
                                instInfo.methodClass = methodClass
3✔
659
                                local tempMethodClassInfo = methodClassInfo
3✔
660
                                while tempMethodClassInfo do
9✔
661
                                        tempMethodClassInfo.inClassFunc = tempMethodClassInfo.inClassFunc + 1
6✔
662
                                        local superclass = tempMethodClassInfo.superclass
6✔
663
                                        tempMethodClassInfo = superclass and private.classInfo[superclass] or nil
6✔
664
                                end
665
                                return private.InstMethodReturnHelper(prevClass, instInfo, methodClassInfo, methodFunc(inst, ...))
3✔
666
                        end
667
                end
668
        end
669
        return instInfo.closures[cacheKey]
6✔
670
end
671

672
function private.InstDump(inst, resultTbl, maxDepth, tableLookupFunc)
1✔
673
        local context = {
2✔
674
                resultTbl = resultTbl,
2✔
675
                maxDepth = maxDepth or 2,
2✔
676
                maxTableEntries = 100,
2✔
677
                tableLookupFunc = tableLookupFunc,
2✔
678
                tableRefs = {},
2✔
679
                depth = 0,
2✔
680
        }
681

682
        -- Build up our table references via a breadth-first search
683
        local bfsQueueKeyPath = {""}
2✔
684
        local bfsQueueDepth = {0}
2✔
685
        local bfsQueueValue = {inst}
2✔
686
        while #bfsQueueKeyPath > 0 do
18✔
687
                local keyPath = tremove(bfsQueueKeyPath, 1)
16✔
688
                local depth = tremove(bfsQueueDepth, 1)
16✔
689
                local value = tremove(bfsQueueValue, 1)
16✔
690
                if not context.tableRefs[value] then
16✔
691
                        context.tableRefs[value] = keyPath
14✔
692
                        if depth <= context.maxDepth and not private.classInfo[value] then
14✔
693
                                if private.instInfo[value] then
10✔
694
                                        local instInfo = private.instInfo[value]
2✔
695
                                        value = instInfo.hasSuperclass and instInfo.fields or value
2✔
696
                                end
697
                                for k, v in pairs(value) do
38✔
698
                                        if type(v) == "table" and (type(k) == "string" or type(k) == "number") and not strfind(k, DUMP_KEY_PATH_DELIM, nil, true) then
28✔
699
                                                tinsert(bfsQueueKeyPath, keyPath..DUMP_KEY_PATH_DELIM..k)
14✔
700
                                                tinsert(bfsQueueDepth, depth + 1)
14✔
701
                                                tinsert(bfsQueueValue, v)
14✔
702
                                        end
703
                                end
704
                        end
705
                end
706
        end
707

708
        private.InstDumpVariable("self", inst, context, "")
2✔
709
end
710

711
function private.InstDumpVariable(key, value, context, strKeyPath)
1✔
712
        if strfind(key, DUMP_KEY_PATH_DELIM, nil, true) then
30✔
713
                -- Ignore keys with our deliminator in them
UNCOV
714
                return
×
715
        end
716
        if type(value) == "table" and private.classInfo[value] then
30✔
717
                -- This is a class
718
                private.InstDumpKeyValue(key, "\""..tostring(value).."\"", context)
2✔
719
        elseif type(value) == "table" then
28✔
720
                local refKeyPath = context.tableRefs[value]
14✔
721
                if not refKeyPath then
14✔
UNCOV
722
                        return
×
723
                end
724
                if refKeyPath ~= strKeyPath then
14✔
725
                        local refValue = "\"REF{"..gsub(refKeyPath, DUMP_KEY_PATH_DELIM, ".").."}\""
2✔
726
                        private.InstDumpKeyValue(key, refValue, context)
2✔
727
                elseif private.instInfo[value] then
12✔
728
                        -- This is an instance of a class
729
                        if context.depth <= context.maxDepth then
2✔
730
                                -- Recurse into the class
731
                                local instInfo = private.instInfo[value]
2✔
732
                                local tbl = instInfo.hasSuperclass and instInfo.fields or value
2✔
733
                                private.InstDumpLine(key.." = <"..instInfo.str.."> {", context)
2✔
734
                                context.depth = context.depth + 1
2✔
735
                                for key2, value2 in pairs(tbl) do
16✔
736
                                        if type(key2) == "string" or type(key2) == "number" or type(key2) == "boolean" then
14✔
737
                                                key2 = tostring(key2)
14✔
738
                                                private.InstDumpVariable(key2, value2, context, strKeyPath..DUMP_KEY_PATH_DELIM..key2)
14✔
739
                                        end
740
                                end
741
                                context.depth = context.depth - 1
2✔
742
                                private.InstDumpLine("}", context)
2✔
743
                        else
UNCOV
744
                                private.InstDumpKeyValue(key, "\""..tostring(value).."\"", context)
×
745
                        end
746
                else
747
                        local isEmpty = true
10✔
748
                        for _, value2 in pairs(value) do
10✔
749
                                local valueType = type(value2)
8✔
750
                                if valueType == "string" or valueType == "number" or valueType == "boolean" or valueType == "table" then
8✔
751
                                        isEmpty = false
8✔
752
                                        break
8✔
753
                                end
754
                        end
755
                        if isEmpty then
10✔
756
                                local info = context.tableLookupFunc and context.tableLookupFunc(value) or nil
2✔
757
                                if info and context.depth <= context.maxDepth then
2✔
758
                                        -- Display the table values
759
                                        private.InstDumpKeyValue(key, "{", context)
×
UNCOV
760
                                        context.depth = context.depth + 1
×
UNCOV
761
                                        for _, line in ipairs({strsplit("\n", info)}) do
×
UNCOV
762
                                                private.InstDumpLine(line, context)
×
763
                                        end
UNCOV
764
                                        context.depth = context.depth - 1
×
UNCOV
765
                                        private.InstDumpLine("}", context)
×
766
                                elseif info then
2✔
UNCOV
767
                                        private.InstDumpKeyValue(key, "{ ... }", context)
×
768
                                else
769
                                        private.InstDumpKeyValue(key, "{}", context)
2✔
770
                                end
771
                        else
772
                                if context.depth <= context.maxDepth then
8✔
773
                                        -- Recurse into the table
774
                                        private.InstDumpKeyValue(key, "{", context)
6✔
775
                                        context.depth = context.depth + 1
6✔
776
                                        local numTableEntries = 0
6✔
777
                                        for key2, value2 in pairs(value) do
20✔
778
                                                if numTableEntries >= context.maxTableEntries then
14✔
779
                                                        break
780
                                                end
781
                                                if type(key2) == "string" or type(key2) == "number" or type(key2) == "boolean" then
14✔
782
                                                        numTableEntries = numTableEntries + 1
14✔
783
                                                        key2 = tostring(key2)
14✔
784
                                                        private.InstDumpVariable(key2, value2, context, strKeyPath..DUMP_KEY_PATH_DELIM..key2)
14✔
785
                                                end
786
                                        end
787
                                        context.depth = context.depth - 1
6✔
788
                                        private.InstDumpLine("}", context)
6✔
789
                                else
790
                                        private.InstDumpKeyValue(key, "{ ... }", context)
2✔
791
                                end
792
                        end
793
                end
794
        elseif type(value) == "string" then
14✔
795
                private.InstDumpKeyValue(key, "\""..value.."\"", context)
2✔
796
        elseif type(value) == "number" or type(value) == "boolean" then
12✔
797
                private.InstDumpKeyValue(key, value, context)
10✔
798
        end
799
end
800

801
function private.InstDumpLine(line, context)
1✔
802
        line = strrep("  ", context.depth)..line
36✔
803
        if context.resultTbl then
36✔
804
                tinsert(context.resultTbl, line)
18✔
805
        else
806
                print(line)
18✔
807
        end
808
end
809

810
function private.InstDumpKeyValue(key, value, context)
1✔
811
        key = tostring(key)
26✔
812
        if key == "" then
26✔
813
                key = "\"\""
2✔
814
        end
815
        if not context.resultTbl then
26✔
816
                key = "|cff88ccff"..key.."|r"
13✔
817
        end
818
        value = tostring(value)
26✔
819
        local line = format("%s = %s", key, value)
26✔
820
        private.InstDumpLine(line, context)
26✔
821
end
822

823

824

825
-- ============================================================================
826
-- Initialization Code
827
-- ============================================================================
828

829
do
830
        -- Register with LibStub
831
        local libStubTbl = LibStub:NewLibrary("LibTSMClass", MINOR_REVISION) --[[@as LibTSMClass?]]
1✔
832
        if libStubTbl then
1✔
833
                for k, v in pairs(Lib) do
4✔
834
                        libStubTbl[k] = v
3✔
835
                end
836
        end
837
        -- Return the library and our private table for unit testing
838
        return {Lib, private}
1✔
839
end
840

841

842

843
-- ============================================================================
844
-- Class Type (LS Support Only)
845
-- ============================================================================
846

847
---@diagnostic disable: missing-return
848

849
---@class Class<S>
850
---@overload fun(): Class<S>
851
---@constructor __init
852
---@field __class Class
853
---@field __name string
854
---@field __super S
855
---@field __tostring fun(self): string
856
---@field __dump fun(self)
857
---@accessor __private private
858
---@accessor __protected protected
859
---@accessor __abstract protected
860
---@accessor __static
NEW
861
local Class = {}
×
862

863
---Returns a closure for the given method that bypasses access controls and passes self.
864
---@protected
865
---@generic K: keyof self
866
---@param name K
867
---@return function
NEW
868
function Class:__closure(name) end
×
869

870
---Checks if the current instance is a class (or a subclass of it).
871
---@generic C: Class
872
---@param class C
873
---@type-narrows 0 1
874
---@return boolean
NEW
875
function Class:__isa(class) end
×
876

877
---Casts the current instance to a parent class.
878
---@generic C: Class, T: C
879
---@param self T
880
---@param class C
881
---@return T
NEW
882
function Class.__as(self, class) end
×
883

884
---Checks if the current instance is equal to another (can be overridden).
885
---@param other any
886
---@return boolean
NEW
887
function Class:__equals(other) end
×
888

889
---@diagnostic enable: missing-return
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