@defold-typescript/library-types 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/api-doc/boom.json +3870 -0
  2. package/api-doc/{bridge.bridge.json → bridge.json} +754 -844
  3. package/api-doc/bzAnim.json +338 -0
  4. package/api-doc/decore.json +23 -23
  5. package/api-doc/defcon.json +150 -0
  6. package/api-doc/{defmath.defmath.json → defmath.json} +6 -46
  7. package/api-doc/defsave.json +518 -0
  8. package/api-doc/deftest.json +741 -0
  9. package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
  10. package/api-doc/druid.json +249 -1846
  11. package/api-doc/event.json +1074 -0
  12. package/api-doc/gooey.json +2050 -0
  13. package/api-doc/immutable.json +65 -0
  14. package/api-doc/in.accelerometer.json +75 -1
  15. package/api-doc/in.button.json +17 -10
  16. package/api-doc/in.cursor.json +26 -0
  17. package/api-doc/in.gesture.json +401 -6
  18. package/api-doc/in.keyboard.json +2 -0
  19. package/api-doc/in.mapper.json +12 -8
  20. package/api-doc/in.onscreen.json +61 -5
  21. package/api-doc/in.state.json +61 -13
  22. package/api-doc/in.textbox.json +23 -12
  23. package/api-doc/in.triggers.json +336 -0
  24. package/api-doc/lang.json +528 -0
  25. package/api-doc/{event.event.json → log.json} +96 -101
  26. package/api-doc/metrics.fps.json +61 -2
  27. package/api-doc/metrics.mem.json +61 -2
  28. package/api-doc/monarch.monarch.json +249 -7
  29. package/api-doc/monarch.transitions.easings.json +19 -26
  30. package/api-doc/monarch.transitions.gui.json +782 -79
  31. package/api-doc/nakama.engine.defold.json +31 -18
  32. package/api-doc/{nakama.nakama.json → nakama.json} +4752 -2554
  33. package/api-doc/nakama.util.log.json +32 -4
  34. package/api-doc/narrator.json +647 -0
  35. package/api-doc/openapi/nakama.nakama.json +7680 -0
  36. package/api-doc/orthographic.camera.json +96 -98
  37. package/api-doc/orthographic.json +602 -0
  38. package/api-doc/panthera.json +1183 -0
  39. package/api-doc/{persist.persist.json → persist.json} +26 -1
  40. package/api-doc/{platypus.platypus.json → platypus.json} +68 -14
  41. package/api-doc/proto.json +995 -0
  42. package/api-doc/{rendy.rendy.json → rendy.json} +46 -99
  43. package/api-doc/richtext.color.json +77 -0
  44. package/api-doc/richtext.richtext.json +375 -5
  45. package/api-doc/saver.saver.json +714 -263
  46. package/api-doc/saver.storage.json +283 -52
  47. package/api-doc/squid.json +846 -0
  48. package/api-doc/{starly.starly.json → starly.json} +1 -1
  49. package/api-doc/tweener.json +277 -0
  50. package/api-doc/{yagames.yagames.json → yagames.json} +784 -189
  51. package/api-doc/{zzfx.api.json → zzfx.json} +9 -9
  52. package/authored-targets.json +403 -0
  53. package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
  54. package/generated/bridge.d.ts +468 -0
  55. package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
  56. package/generated/decore.d.ts +37 -37
  57. package/generated/defcon.d.ts +43 -0
  58. package/generated/{defmath.defmath.d.ts → defmath.d.ts} +3 -2
  59. package/generated/defsave.d.ts +144 -0
  60. package/generated/{dicebag.dicebag.d.ts → dicebag.d.ts} +25 -0
  61. package/generated/druid.d.ts +198 -502
  62. package/generated/event.d.ts +318 -0
  63. package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
  64. package/generated/immutable.d.ts +13 -0
  65. package/generated/in.onscreen.d.ts +5 -0
  66. package/generated/in.textbox.d.ts +4 -0
  67. package/generated/lang.d.ts +101 -0
  68. package/generated/log.d.ts +36 -0
  69. package/generated/metrics.fps.d.ts +19 -1
  70. package/generated/metrics.mem.d.ts +19 -1
  71. package/generated/monarch.monarch.d.ts +44 -1
  72. package/generated/monarch.transitions.easings.d.ts +0 -1
  73. package/generated/monarch.transitions.gui.d.ts +96 -12
  74. package/generated/nakama.d.ts +1158 -0
  75. package/generated/nakama.engine.defold.d.ts +3 -1
  76. package/generated/nakama.util.log.d.ts +6 -0
  77. package/generated/narrator.d.ts +121 -0
  78. package/generated/openapi/nakama.nakama.d.ts +884 -0
  79. package/generated/orthographic.camera.d.ts +24 -38
  80. package/generated/orthographic.d.ts +155 -0
  81. package/generated/panthera.d.ts +170 -0
  82. package/generated/{persist.persist.d.ts → persist.d.ts} +3 -0
  83. package/generated/{platypus.platypus.d.ts → platypus.d.ts} +9 -0
  84. package/generated/proto.d.ts +146 -0
  85. package/generated/{rendy.rendy.d.ts → rendy.d.ts} +33 -80
  86. package/generated/richtext.color.d.ts +15 -0
  87. package/generated/saver.saver.d.ts +289 -42
  88. package/generated/saver.storage.d.ts +79 -14
  89. package/generated/squid.d.ts +127 -0
  90. package/generated/tweener.d.ts +42 -0
  91. package/generated/{yagames.yagames.d.ts → yagames.d.ts} +151 -29
  92. package/generated/{zzfx.api.d.ts → zzfx.d.ts} +7 -0
  93. package/library-classification.json +0 -227
  94. package/library-targets.json +1 -284
  95. package/luals-targets.json +168 -1
  96. package/openapi-targets.json +17 -0
  97. package/package.json +9 -88
  98. package/script-api-targets.json +15 -0
  99. package/scripts/__snapshots__/parse-luals.test.ts.snap +504 -85
  100. package/scripts/apply-luals-overrides.ts +95 -0
  101. package/scripts/authored-parity.ts +718 -0
  102. package/scripts/emit-library-dts.ts +177 -19
  103. package/scripts/extract-api-doc.ts +189 -38
  104. package/scripts/fidelity-floor.ts +178 -0
  105. package/scripts/import-upstream-docs.ts +125 -0
  106. package/scripts/lower-api-doc.ts +69 -19
  107. package/scripts/luals-fidelity.ts +22 -10
  108. package/scripts/map-luals-types.ts +86 -22
  109. package/scripts/parse-lua-surface.ts +187 -0
  110. package/scripts/parse-luals.ts +473 -22
  111. package/scripts/parse-markdown-api.ts +247 -0
  112. package/scripts/parse-openapi-api.ts +313 -0
  113. package/scripts/sync-authored-types.ts +249 -0
  114. package/scripts/sync-library-types.ts +99 -3
  115. package/scripts/sync-luals-types.ts +29 -2
  116. package/scripts/sync-markdown-types.ts +761 -0
  117. package/scripts/sync-openapi-types.ts +308 -0
  118. package/scripts/sync-script-api-types.ts +367 -0
  119. package/api-doc/boom.boom.json +0 -26
  120. package/api-doc/bzAnim.bzLibrary.json +0 -156
  121. package/api-doc/defcon.console.json +0 -109
  122. package/api-doc/defsave.defsave.json +0 -175
  123. package/api-doc/deftest.deftest.json +0 -71
  124. package/api-doc/gooey.gooey.json +0 -963
  125. package/api-doc/immutable.immutable.json +0 -63
  126. package/api-doc/lang.lang.json +0 -411
  127. package/api-doc/log.log.json +0 -50
  128. package/api-doc/narrator.narrator.json +0 -150
  129. package/api-doc/proto.proto.json +0 -355
  130. package/api-doc/squid.squid.json +0 -660
  131. package/api-doc/tweener.tweener.json +0 -419
  132. package/generated/bridge.bridge.d.ts +0 -533
  133. package/generated/defcon.console.d.ts +0 -24
  134. package/generated/defsave.defsave.d.ts +0 -31
  135. package/generated/event.event.d.ts +0 -54
  136. package/generated/immutable.immutable.d.ts +0 -13
  137. package/generated/lang.lang.d.ts +0 -33
  138. package/generated/log.log.d.ts +0 -40
  139. package/generated/nakama.nakama.d.ts +0 -594
  140. package/generated/narrator.narrator.d.ts +0 -66
  141. package/generated/proto.proto.d.ts +0 -36
  142. package/generated/squid.squid.d.ts +0 -106
  143. package/generated/tweener.tweener.d.ts +0 -151
  144. /package/generated/{deftest.deftest.d.ts → deftest.d.ts} +0 -0
  145. /package/generated/{starly.starly.d.ts → starly.d.ts} +0 -0
@@ -16,6 +16,11 @@ export interface LibraryModel {
16
16
  interfaces: LibraryInterface[];
17
17
  aliases: LibraryAlias[];
18
18
  moduleFunctions: LibraryMethod[];
19
+ // The name of the `---@class` a `return <name>` at column 0 hands back as the module
20
+ // table. Its public fields are the module's own constants (`export const`s), not a
21
+ // standalone interface. Set only when a returned local resolves to an opened class,
22
+ // so a module with a plain returned table carries no key.
23
+ moduleObject?: string;
19
24
  }
20
25
 
21
26
  export interface LibraryInterface {
@@ -25,6 +30,17 @@ export interface LibraryInterface {
25
30
  fields: LibraryField[];
26
31
  methods: LibraryMethod[];
27
32
  brief: string;
33
+ // A class-level `---@overload fun(...)`, kept as its raw `fun(...)` token plus the
34
+ // trailing description. Present only on interfaces that declare one (like `extends`),
35
+ // so an interface without overloads carries no key. The emitter renders each as an
36
+ // interface call signature; the mapper maps the token to a `(params): ret` form.
37
+ overloads?: LibraryOverload[];
38
+ deprecated?: string;
39
+ }
40
+
41
+ export interface LibraryOverload {
42
+ type: string;
43
+ doc: string;
28
44
  }
29
45
 
30
46
  export interface LibraryMethod {
@@ -33,6 +49,15 @@ export interface LibraryMethod {
33
49
  generics: LibraryGeneric[];
34
50
  params: LibraryParam[];
35
51
  returns: LibraryParam[];
52
+ // A standalone `---@local`/`---@private`/`---@protected`/`---@package` before the
53
+ // function declaration. Set only when marked (like a field's `visibility`), so an
54
+ // unmarked method carries no key. `local` has no `@field` analogue (LuaLS field
55
+ // scope has no `local`), so a method's visibility widens the field set with it.
56
+ visibility?: LibraryMethodVisibility;
57
+ // A `---@deprecated` on the pending block. Present exactly when the tag is, with
58
+ // `""` for the bare form — absence of the key is the sole encoding of "not
59
+ // deprecated", so a bare tag must never be collapsed away by a truthiness guard.
60
+ deprecated?: string;
36
61
  }
37
62
 
38
63
  export interface LibraryParam {
@@ -41,10 +66,17 @@ export interface LibraryParam {
41
66
  doc: string;
42
67
  isOptional: boolean;
43
68
  isVararg: boolean;
69
+ // True when the raw type token carries a top-level `nil` union member (`T|nil`),
70
+ // distinct from the literal trailing `?` that drives `isOptional`. Set only when
71
+ // true (like a field's `visibility`), so a non-nil-bearing param carries no key.
72
+ // The emitter's trailing-run rule treats `isOptional || isNilable` as omittable.
73
+ isNilable?: boolean;
44
74
  }
45
75
 
46
76
  export type LibraryFieldVisibility = "public" | "protected" | "private" | "package";
47
77
 
78
+ export type LibraryMethodVisibility = LibraryFieldVisibility | "local";
79
+
48
80
  export interface LibraryField {
49
81
  name: string;
50
82
  types: string[];
@@ -62,6 +94,7 @@ export interface LibraryAlias {
62
94
  name: string;
63
95
  types: string[];
64
96
  doc: string;
97
+ deprecated?: string;
65
98
  }
66
99
 
67
100
  interface Pending {
@@ -69,9 +102,18 @@ interface Pending {
69
102
  params: LibraryParam[];
70
103
  returns: LibraryParam[];
71
104
  generics: LibraryGeneric[];
105
+ overloads: LibraryOverload[];
106
+ visibility?: LibraryMethodVisibility;
107
+ deprecated?: string;
72
108
  }
73
109
 
74
- const emptyPending = (): Pending => ({ doc: [], params: [], returns: [], generics: [] });
110
+ const emptyPending = (): Pending => ({
111
+ doc: [],
112
+ params: [],
113
+ returns: [],
114
+ generics: [],
115
+ overloads: [],
116
+ });
75
117
 
76
118
  /**
77
119
  * Read a single raw type token from the head of `rest`, honoring bracket depth so
@@ -101,6 +143,170 @@ function readTypeToken(rest: string): { type: string; rest: string } {
101
143
  return { type: rest.slice(0, i), rest: rest.slice(i).trim() };
102
144
  }
103
145
 
146
+ /** Index of the bracket matching the opener at `open`, or -1 if unbalanced. */
147
+ function matchCloser(s: string, open: number): number {
148
+ const pairs: Record<string, string> = { "<": ">", "(": ")", "[": "]", "{": "}" };
149
+ const want = pairs[s[open] as string];
150
+ let depth = 0;
151
+ let inQuote = false;
152
+ for (let i = open; i < s.length; i++) {
153
+ const c = s[i];
154
+ if (inQuote) {
155
+ if (c === '"') inQuote = false;
156
+ continue;
157
+ }
158
+ if (c === '"') inQuote = true;
159
+ else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
160
+ else if (c === ">" || c === ")" || c === "]" || c === "}") {
161
+ depth--;
162
+ if (depth === 0) return c === want ? i : -1;
163
+ }
164
+ }
165
+ return -1;
166
+ }
167
+
168
+ /**
169
+ * Split `s` on every top-level occurrence of the single-character `sep`, honoring
170
+ * bracket depth and double-quoted string literals so a separator nested inside
171
+ * `<...>`, `(...)`, `[...]`, `{...}`, or a `"..."` literal does not split. A parser-
172
+ * local copy of the mapper's identical helper — the parser is upstream of the mapper
173
+ * and must not import it (`hasTopLevelNil`'s comment).
174
+ */
175
+ function splitTopLevel(s: string, sep: string): string[] {
176
+ const parts: string[] = [];
177
+ let depth = 0;
178
+ let inQuote = false;
179
+ let start = 0;
180
+ for (let i = 0; i < s.length; i++) {
181
+ const c = s[i];
182
+ if (inQuote) {
183
+ if (c === '"') inQuote = false;
184
+ continue;
185
+ }
186
+ if (c === '"') inQuote = true;
187
+ else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
188
+ else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
189
+ else if (depth === 0 && c === sep) {
190
+ parts.push(s.slice(start, i));
191
+ start = i + 1;
192
+ }
193
+ }
194
+ parts.push(s.slice(start));
195
+ return parts;
196
+ }
197
+
198
+ /**
199
+ * Decompose a `fun(...)` token into a method's `params`/`returns`, dropping a leading
200
+ * `self`. Used for a function-local `---@class` member typed by `---@type fun(...)`:
201
+ * modeling it as a method (each param keeping its raw type token) rather than a field
202
+ * makes the emitter render a full-typed method instead of the lossy permissive hook
203
+ * `matchSelfHookField` would produce. An unbalanced token yields empty lists.
204
+ */
205
+ function funToMethodParts(token: string): { params: LibraryParam[]; returns: LibraryParam[] } {
206
+ const open = token.indexOf("(");
207
+ const close = open === -1 ? -1 : matchCloser(token, open);
208
+ if (open === -1 || close === -1) return { params: [], returns: [] };
209
+ const paramsStr = token.slice(open + 1, close).trim();
210
+ const afterClose = token.slice(close + 1).trim();
211
+
212
+ const params: LibraryParam[] = [];
213
+ const rawParams = paramsStr === "" ? [] : splitTopLevel(paramsStr, ",");
214
+ for (const rawPart of rawParams) {
215
+ const part = rawPart.trim();
216
+ if (part === "") continue;
217
+ if (part.startsWith("...")) {
218
+ const after = part.slice(3).trim();
219
+ const type = after.startsWith(":") ? after.slice(1).trim() : "";
220
+ const vararg: LibraryParam = {
221
+ name: "...",
222
+ types: type ? [type] : [],
223
+ doc: "",
224
+ isOptional: false,
225
+ isVararg: true,
226
+ };
227
+ if (type && hasTopLevelNil(type)) vararg.isNilable = true;
228
+ params.push(vararg);
229
+ continue;
230
+ }
231
+ const colon = splitTopLevel(part, ":");
232
+ const rawName = (colon[0] ?? "").trim();
233
+ const typeExpr = colon.length >= 2 ? colon.slice(1).join(":").trim() : "";
234
+ const isOptional = rawName.endsWith("?");
235
+ const name = isOptional ? rawName.slice(0, -1) : rawName;
236
+ const param: LibraryParam = {
237
+ name,
238
+ types: typeExpr ? [typeExpr] : [],
239
+ doc: "",
240
+ isOptional,
241
+ isVararg: false,
242
+ };
243
+ if (typeExpr && hasTopLevelNil(typeExpr)) param.isNilable = true;
244
+ params.push(param);
245
+ }
246
+ if (params[0]?.name === "self") params.shift();
247
+
248
+ const returns: LibraryParam[] = [];
249
+ if (afterClose.startsWith(":")) {
250
+ const retStr = afterClose.slice(1).trim();
251
+ for (const raw of retStr === "" ? [] : splitTopLevel(retStr, ",")) {
252
+ const type = raw.trim();
253
+ if (type)
254
+ returns.push({ name: "", types: [type], doc: "", isOptional: false, isVararg: false });
255
+ }
256
+ }
257
+ return { params, returns };
258
+ }
259
+
260
+ /**
261
+ * True when the raw type token has a top-level `nil` union member (`T|nil`,
262
+ * `fun()|nil`) — the signal that a parameter is nil-bearing and can be emitted
263
+ * TS-optional. Bracket- and quote-depth aware so a `nil` nested in
264
+ * `table<...>`/`{...}`/a `"..."` literal does not count, and a `fun(...): ret|nil`
265
+ * return-union (whose `|nil` sits at depth 0 after the `)`) is recognized as the
266
+ * function's own return, not an outer nullable — only a `|nil` applied to the whole
267
+ * token flags the param. Self-contained: the parser is upstream of the mapper and
268
+ * must not import it.
269
+ */
270
+ function hasTopLevelNil(rawToken: string): boolean {
271
+ let token = rawToken.trim();
272
+ while (token.startsWith("(") && matchCloser(token, 0) === token.length - 1) {
273
+ token = token.slice(1, -1).trim();
274
+ }
275
+ if (/^fun\s*\(/.test(token)) {
276
+ const close = matchCloser(token, token.indexOf("("));
277
+ if (
278
+ close !== -1 &&
279
+ token
280
+ .slice(close + 1)
281
+ .trim()
282
+ .startsWith(":")
283
+ )
284
+ return false;
285
+ }
286
+ // A type-suffix `T?` (`string?`, `fun()?`, `(a|b)?`) is nil-bearing. Placed after the
287
+ // fun-return guard so a nullable return (`fun(): a|nil`, `fun(): string?`) never flags.
288
+ if (token.endsWith("?")) return true;
289
+ const isNilSeg = (from: number, to: number): boolean => token.slice(from, to).trim() === "nil";
290
+ let depth = 0;
291
+ let inQuote = false;
292
+ let segStart = 0;
293
+ for (let i = 0; i < token.length; i++) {
294
+ const c = token[i];
295
+ if (inQuote) {
296
+ if (c === '"') inQuote = false;
297
+ continue;
298
+ }
299
+ if (c === '"') inQuote = true;
300
+ else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
301
+ else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
302
+ else if (depth === 0 && c === "|") {
303
+ if (isNilSeg(segStart, i)) return true;
304
+ segStart = i + 1;
305
+ }
306
+ }
307
+ return isNilSeg(segStart, token.length);
308
+ }
309
+
104
310
  /** A bare lowercase identifier — the shape druid uses for an optional `@return` name. */
105
311
  const RETURN_NAME = /^[a-z_][A-Za-z0-9_]*$/;
106
312
 
@@ -112,10 +318,19 @@ function parseParam(rest: string): LibraryParam {
112
318
  const isOptional = !isVararg && rawName.endsWith("?");
113
319
  const name = isOptional ? rawName.slice(0, -1) : rawName;
114
320
  const { type, rest: doc } = readTypeToken(afterName);
115
- return { name, types: type ? [type] : [], doc, isOptional, isVararg };
321
+ const param: LibraryParam = { name, types: type ? [type] : [], doc, isOptional, isVararg };
322
+ if (type && hasTopLevelNil(type)) param.isNilable = true;
323
+ return param;
116
324
  }
117
325
 
118
- function parseReturn(rest: string): LibraryParam {
326
+ /**
327
+ * A single `---@return` line may declare several values (`---@return number, number`),
328
+ * which `readTypeToken`'s comma continuation hands back as one token. Split it on
329
+ * top-level commas into one entry per value; LuaLS attaches the optional trailing name
330
+ * and description to the whole line, so only the last segment carries them. The
331
+ * emitter's `renderReturn` already lowers a multi-entry `returns` to `LuaMultiReturn`.
332
+ */
333
+ function parseReturn(rest: string): LibraryParam[] {
119
334
  const { type, rest: afterType } = readTypeToken(rest);
120
335
  const spaceAt = afterType.search(/\s/);
121
336
  const head = spaceAt === -1 ? afterType : afterType.slice(0, spaceAt);
@@ -125,7 +340,22 @@ function parseReturn(rest: string): LibraryParam {
125
340
  name = head;
126
341
  doc = spaceAt === -1 ? "" : afterType.slice(spaceAt).trim();
127
342
  }
128
- return { name, types: type ? [type] : [], doc, isOptional: false, isVararg: false };
343
+ const segments = (type ? splitTopLevel(type, ",") : [""])
344
+ .map((segment) => segment.trim())
345
+ .filter((segment) => segment !== "");
346
+ if (segments.length === 0) {
347
+ return [{ name, types: [], doc, isOptional: false, isVararg: false }];
348
+ }
349
+ return segments.map((segment, index) => {
350
+ const isLast = index === segments.length - 1;
351
+ return {
352
+ name: isLast ? name : "",
353
+ types: [segment],
354
+ doc: isLast ? doc : "",
355
+ isOptional: false,
356
+ isVararg: false,
357
+ };
358
+ });
129
359
  }
130
360
 
131
361
  const VISIBILITY_KEYWORDS = new Set<LibraryFieldVisibility>([
@@ -152,9 +382,12 @@ function parseField(rest: string): LibraryField {
152
382
  const spaceAt = body.search(/\s/);
153
383
  const rawName = spaceAt === -1 ? body : body.slice(0, spaceAt);
154
384
  const afterName = spaceAt === -1 ? "" : body.slice(spaceAt).trim();
155
- const isOptional = rawName.endsWith("?");
156
- const name = isOptional ? rawName.slice(0, -1) : rawName;
385
+ const nameSuffix = rawName.endsWith("?");
386
+ const name = nameSuffix ? rawName.slice(0, -1) : rawName;
157
387
  const { type, rest: doc } = readTypeToken(afterName);
388
+ // Interface properties carry no optional-before-required rule, so a `|nil`/`?` type
389
+ // folds straight into the field's optionality.
390
+ const isOptional = nameSuffix || (type !== "" && hasTopLevelNil(type));
158
391
  return {
159
392
  name,
160
393
  types: type ? [type] : [],
@@ -166,7 +399,15 @@ function parseField(rest: string): LibraryField {
166
399
 
167
400
  function parseVararg(rest: string): LibraryParam {
168
401
  const { type, rest: doc } = readTypeToken(rest);
169
- return { name: "...", types: type ? [type] : [], doc, isOptional: false, isVararg: true };
402
+ const param: LibraryParam = {
403
+ name: "...",
404
+ types: type ? [type] : [],
405
+ doc,
406
+ isOptional: false,
407
+ isVararg: true,
408
+ };
409
+ if (type && hasTopLevelNil(type)) param.isNilable = true;
410
+ return param;
170
411
  }
171
412
 
172
413
  function parseGenerics(rest: string): LibraryGeneric[] {
@@ -181,12 +422,50 @@ function parseGenerics(rest: string): LibraryGeneric[] {
181
422
  });
182
423
  }
183
424
 
184
- /** Parse a `@class Name[ : parent]` head. The parent is kept as a single raw token. */
425
+ /**
426
+ * Read one class-head token from `s` starting at `start`, tracking `<[({` depth so a
427
+ * generic like `Bar<A, B>` stays whole. Ends at the first top-level space/tab, `:`, or
428
+ * `,` (none consumed). Returns the trimmed token and its end index. Distinct from
429
+ * `readTypeToken`, whose `fun(...)` `:`/`,` continuation rule wrongly glues a
430
+ * `Name: Parent` head together.
431
+ */
432
+ function readClassToken(s: string, start: number): { token: string; end: number } {
433
+ let depth = 0;
434
+ let i = start;
435
+ for (; i < s.length; i++) {
436
+ const c = s[i];
437
+ if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
438
+ else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
439
+ else if (depth === 0 && (c === " " || c === "\t" || c === ":" || c === ",")) break;
440
+ }
441
+ return { token: s.slice(start, i).trim(), end: i };
442
+ }
443
+
444
+ /**
445
+ * Parse a `@class Name[: parent[, parent...]] [description]` head. The identifier and
446
+ * optional parent list are read as bracket-aware tokens; any trailing human
447
+ * description is dropped. Multiple parents are joined with `", "` to match the single
448
+ * `extends` string the emitter renders.
449
+ */
185
450
  function parseClassHead(rest: string): { name: string; extends?: string } {
186
- const colon = rest.indexOf(":");
187
- if (colon === -1) return { name: rest.trim() };
188
- const parent = rest.slice(colon + 1).trim();
189
- return { name: rest.slice(0, colon).trim(), ...(parent ? { extends: parent } : {}) };
451
+ const s = rest.trim();
452
+ const { token: name, end: afterName } = readClassToken(s, 0);
453
+ let i = afterName;
454
+ while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
455
+ if (s[i] !== ":") return { name };
456
+ i++;
457
+ const parents: string[] = [];
458
+ while (true) {
459
+ while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
460
+ const { token, end } = readClassToken(s, i);
461
+ if (token !== "") parents.push(token);
462
+ i = end;
463
+ while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
464
+ if (s[i] !== ",") break;
465
+ i++;
466
+ }
467
+ const extendsStr = parents.join(", ");
468
+ return { name, ...(extendsStr ? { extends: extendsStr } : {}) };
190
469
  }
191
470
 
192
471
  interface FunctionDecl {
@@ -260,6 +539,19 @@ export function parseLualsSource(source: string): LibraryModel {
260
539
  let pending = emptyPending();
261
540
  let openClass: LibraryInterface | null = null;
262
541
  let lastOpenedClass: string | null = null;
542
+ let moduleObject: string | undefined;
543
+ let lastModuleFunction: LibraryMethod | null = null;
544
+ // The one open function-local `---@class` block (squid's `SquidInstance` inside
545
+ // `Squid.new`). `owner` is the function whose body it sits in, so a returned local
546
+ // bound to it infers the function's return; `pendingType` is a `---@type fun(...)`
547
+ // armed for the next member key.
548
+ let localClass: {
549
+ iface: LibraryInterface;
550
+ localVar: string | null;
551
+ owner: LibraryMethod | null;
552
+ pendingType: string | null;
553
+ returnedSelf: boolean;
554
+ } | null = null;
263
555
 
264
556
  const ensureInterface = (name: string): LibraryInterface => {
265
557
  const existing = byName.get(name);
@@ -282,11 +574,94 @@ export function parseLualsSource(source: string): LibraryModel {
282
574
  generics: pending.generics,
283
575
  params: pending.params,
284
576
  returns: pending.returns,
577
+ ...(pending.visibility ? { visibility: pending.visibility } : {}),
578
+ ...(pending.deprecated !== undefined ? { deprecated: pending.deprecated } : {}),
285
579
  });
286
580
 
581
+ // Interpret one indented line while a function-local `---@class` block is open. Only
582
+ // the `---@type fun(...)` + next `<key> = ...` member pattern, the backing `local`,
583
+ // and the `return <local>` are recognized; every other indented line stays opaque.
584
+ const handleLocalClassLine = (line: string): void => {
585
+ const lc = localClass;
586
+ if (!lc) return;
587
+ const typeMatch = /^---@type\s+(.+)$/.exec(line);
588
+ if (typeMatch) {
589
+ const { type } = readTypeToken(typeMatch[1] ?? "");
590
+ // A non-`fun` `---@type` also disarms a previously-armed one.
591
+ lc.pendingType = /^fun\s*\(/.test(type) ? type : null;
592
+ return;
593
+ }
594
+ if (lc.pendingType) {
595
+ const keyMatch = /^([A-Za-z_]\w*)\s*=/.exec(line);
596
+ if (keyMatch) {
597
+ const { params, returns } = funToMethodParts(lc.pendingType);
598
+ lc.iface.methods.push({
599
+ name: keyMatch[1] as string,
600
+ brief: "",
601
+ generics: [],
602
+ params,
603
+ returns,
604
+ });
605
+ lc.pendingType = null;
606
+ return;
607
+ }
608
+ // Not the immediately-following member key: the armed type has no member.
609
+ lc.pendingType = null;
610
+ }
611
+ const returnMatch = /^return\s+([A-Za-z_]\w*)\s*$/.exec(line);
612
+ if (returnMatch) {
613
+ if (returnMatch[1] === lc.localVar) lc.returnedSelf = true;
614
+ return;
615
+ }
616
+ const local = LOCAL_ASSIGN.exec(line);
617
+ if (local) {
618
+ // A `---@class` annotates the immediately-following declaration, so only the
619
+ // first local after the class opens is the instance; a later local must not rebind.
620
+ if (lc.localVar === null) lc.localVar = local[1] ?? null;
621
+ return;
622
+ }
623
+ };
624
+
287
625
  for (const raw of source.split("\n")) {
288
- // Column-0 discipline: a line with leading whitespace is opaque to the scanner.
289
- if (/^\s/.test(raw) || raw.length === 0) continue;
626
+ const indented = /^\s/.test(raw);
627
+ if (indented || raw.length === 0) {
628
+ if (localClass) {
629
+ handleLocalClassLine(raw.trim());
630
+ } else if (indented) {
631
+ // Only an indented `---@class` opens function-local capture; every other
632
+ // indented line (druid-style `---@cast`/`---@type` narrowing) stays opaque.
633
+ const classMatch = /^---@class\s+(.+)$/.exec(raw.trim());
634
+ if (classMatch) {
635
+ const head = parseClassHead(classMatch[1] ?? "");
636
+ const iface = ensureInterface(head.name);
637
+ if (head.extends) iface.extends = head.extends;
638
+ localClass = {
639
+ iface,
640
+ localVar: null,
641
+ owner: lastModuleFunction,
642
+ pendingType: null,
643
+ returnedSelf: false,
644
+ };
645
+ }
646
+ }
647
+ continue;
648
+ }
649
+
650
+ // A column-0 line ends any open function-local class (dedent). Apply the inferred
651
+ // return to the owner only when it declared no explicit `---@return`.
652
+ if (localClass) {
653
+ const lc = localClass;
654
+ if (lc.returnedSelf && lc.owner && lc.owner.returns.length === 0) {
655
+ lc.owner.returns.push({
656
+ name: "",
657
+ types: [lc.iface.name],
658
+ doc: "",
659
+ isOptional: false,
660
+ isVararg: false,
661
+ });
662
+ }
663
+ localClass = null;
664
+ }
290
665
 
291
666
  if (raw.startsWith("---@")) {
292
667
  const tagMatch = /^---@([a-zA-Z]+)\s*(.*)$/.exec(raw);
@@ -300,6 +675,8 @@ export function parseLualsSource(source: string): LibraryModel {
300
675
  if (head.extends) iface.extends = head.extends;
301
676
  if (pending.doc.length > 0 && iface.brief === "") iface.brief = pending.doc.join("\n");
302
677
  if (pending.generics.length > 0) iface.generics = pending.generics;
678
+ if (pending.overloads.length > 0) iface.overloads = pending.overloads;
679
+ if (pending.deprecated !== undefined) iface.deprecated = pending.deprecated;
303
680
  openClass = iface;
304
681
  lastOpenedClass = head.name;
305
682
  pending = emptyPending();
@@ -318,24 +695,54 @@ export function parseLualsSource(source: string): LibraryModel {
318
695
  break;
319
696
  }
320
697
  case "return": {
321
- pending.returns.push(parseReturn(rest));
698
+ pending.returns.push(...parseReturn(rest));
322
699
  break;
323
700
  }
324
701
  case "generic": {
325
702
  pending.generics.push(...parseGenerics(rest));
326
703
  break;
327
704
  }
705
+ case "overload": {
706
+ // A class-level `---@overload fun(...)`: keep the raw `fun(...)` token via
707
+ // readTypeToken (its spaced `): ret` return stays whole) plus the trailing
708
+ // doc, and transfer to the interface on the following `@class` — like brief
709
+ // and generics. A non-`fun` overload is outside the modeled subset and dropped.
710
+ const { type, rest: doc } = readTypeToken(rest);
711
+ if (/^fun\s*\(/.test(type)) pending.overloads.push({ type, doc });
712
+ break;
713
+ }
328
714
  case "alias": {
329
715
  const spaceAt = rest.search(/\s/);
330
716
  const name = spaceAt === -1 ? rest : rest.slice(0, spaceAt);
331
717
  const expr = spaceAt === -1 ? "" : rest.slice(spaceAt).trim();
332
- aliases.push({ name, types: expr ? [expr] : [], doc: pending.doc.join("\n") });
718
+ aliases.push({
719
+ name,
720
+ types: expr ? [expr] : [],
721
+ doc: pending.doc.join("\n"),
722
+ ...(pending.deprecated !== undefined ? { deprecated: pending.deprecated } : {}),
723
+ });
333
724
  pending = emptyPending();
334
725
  break;
335
726
  }
727
+ case "private":
728
+ case "protected":
729
+ case "package":
730
+ case "local": {
731
+ // A standalone visibility tag on the pending block marks the next function
732
+ // declaration's method/module-function `visibility` (a `@class` or `@alias`
733
+ // that consumes the block first resets it, so it never leaks onto a type).
734
+ pending.visibility = tag as LibraryMethodVisibility;
735
+ break;
736
+ }
737
+ case "deprecated": {
738
+ // `rest` is already trimmed and is `""` for the bare form, which is the
739
+ // only form the fixtures actually ship; the text form is carried too.
740
+ pending.deprecated = rest;
741
+ break;
742
+ }
336
743
  default:
337
- // @private, @protected, @cast, @type, @diagnostic, @overload, ... — outside
338
- // the Druid subset; recognized as a tag and skipped, never treated as doc.
744
+ // @cast, @type, @diagnostic, ... — outside the Druid subset; recognized as a
745
+ // tag and skipped, never treated as doc.
339
746
  break;
340
747
  }
341
748
  continue;
@@ -348,12 +755,16 @@ export function parseLualsSource(source: string): LibraryModel {
348
755
 
349
756
  const decl = parseFunctionDecl(raw);
350
757
  if (decl) {
758
+ const method = methodFromPending(decl.name);
351
759
  if (decl.kind === "method") {
352
760
  const target = decl.receiver ? (receiverBinding.get(decl.receiver) ?? decl.receiver) : "";
353
- ensureInterface(target).methods.push(methodFromPending(decl.name));
761
+ ensureInterface(target).methods.push(method);
354
762
  } else if (decl.qualified) {
355
- moduleFunctions.push(methodFromPending(decl.name));
763
+ moduleFunctions.push(method);
356
764
  }
765
+ // Any function may host a function-local `---@class`; remember the enclosing
766
+ // function so a returned local can infer its return, even a bare helper.
767
+ lastModuleFunction = method;
357
768
  pending = emptyPending();
358
769
  openClass = null;
359
770
  continue;
@@ -366,10 +777,34 @@ export function parseLualsSource(source: string): LibraryModel {
366
777
  lastOpenedClass = null;
367
778
  openClass = null;
368
779
  pending = emptyPending();
780
+ continue;
781
+ }
782
+
783
+ // A column-0 `return <name>` handing back a local bound to an opened `---@class`
784
+ // marks that class as the module object (its fields become module-level consts).
785
+ // Restricted to squid's constants-table idiom to keep every other library's golden
786
+ // untouched: the returned local is named after its class (`local Squid = {}` /
787
+ // `return Squid`, not a generic `local M` alias), the class carries at least one
788
+ // public field, and it has no methods. A component/instance class (colon methods),
789
+ // an opaque handle (no fields), or an `M`-aliased module table is a type consumers
790
+ // reference or a plain namespace, so it stays a standalone interface.
791
+ const returnStmt = /^return\s+([A-Za-z_][\w.]*)\s*$/.exec(raw);
792
+ if (returnStmt) {
793
+ const name = returnStmt[1] as string;
794
+ const resolvedName = receiverBinding.get(name) ?? (byName.has(name) ? name : undefined);
795
+ const resolved = resolvedName ? byName.get(resolvedName) : undefined;
796
+ if (
797
+ resolved &&
798
+ resolvedName === name &&
799
+ resolved.methods.length === 0 &&
800
+ resolved.fields.some((f) => f.visibility === undefined || f.visibility === "public")
801
+ ) {
802
+ moduleObject = resolved.name;
803
+ }
369
804
  }
370
805
  }
371
806
 
372
- return { interfaces, aliases, moduleFunctions };
807
+ return { interfaces, aliases, moduleFunctions, ...(moduleObject ? { moduleObject } : {}) };
373
808
  }
374
809
 
375
810
  /**
@@ -383,8 +818,10 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
383
818
  const byName = new Map<string, LibraryInterface>();
384
819
  const aliases: LibraryAlias[] = [];
385
820
  const moduleFunctions: LibraryMethod[] = [];
821
+ let moduleObject: string | undefined;
386
822
 
387
823
  for (const model of models) {
824
+ if (!moduleObject && model.moduleObject) moduleObject = model.moduleObject;
388
825
  for (const iface of model.interfaces) {
389
826
  const existing = byName.get(iface.name);
390
827
  if (!existing) {
@@ -395,6 +832,10 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
395
832
  fields: [...iface.fields],
396
833
  methods: [...iface.methods],
397
834
  brief: iface.brief,
835
+ ...(iface.overloads && iface.overloads.length > 0
836
+ ? { overloads: [...iface.overloads] }
837
+ : {}),
838
+ ...(iface.deprecated !== undefined ? { deprecated: iface.deprecated } : {}),
398
839
  };
399
840
  byName.set(iface.name, copy);
400
841
  interfaces.push(copy);
@@ -403,10 +844,20 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
403
844
  existing.fields.push(...iface.fields);
404
845
  existing.methods.push(...iface.methods);
405
846
  if (!existing.extends && iface.extends) existing.extends = iface.extends;
847
+ if (existing.deprecated === undefined && iface.deprecated !== undefined) {
848
+ existing.deprecated = iface.deprecated;
849
+ }
406
850
  if (existing.brief === "" && iface.brief !== "") existing.brief = iface.brief;
407
851
  if (existing.generics.length === 0 && iface.generics.length > 0) {
408
852
  existing.generics = [...iface.generics];
409
853
  }
854
+ if (
855
+ (!existing.overloads || existing.overloads.length === 0) &&
856
+ iface.overloads &&
857
+ iface.overloads.length > 0
858
+ ) {
859
+ existing.overloads = [...iface.overloads];
860
+ }
410
861
  }
411
862
  aliases.push(...model.aliases);
412
863
  moduleFunctions.push(...model.moduleFunctions);
@@ -419,7 +870,7 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
419
870
  // overloaded module functions keep every signature.
420
871
  for (const iface of interfaces) iface.fields = dedupeByName(iface.fields);
421
872
 
422
- return { interfaces, aliases, moduleFunctions };
873
+ return { interfaces, aliases, moduleFunctions, ...(moduleObject ? { moduleObject } : {}) };
423
874
  }
424
875
 
425
876
  function dedupeByName<T extends { name: string }>(items: T[]): T[] {