@defold-typescript/library-types 0.22.0 → 0.23.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 (56) hide show
  1. package/api-doc/{bridge.bridge.json → bridge.json} +749 -844
  2. package/api-doc/decore.json +22 -22
  3. package/api-doc/druid.json +195 -1790
  4. package/api-doc/event.json +1074 -0
  5. package/api-doc/immutable.json +65 -0
  6. package/api-doc/lang.json +528 -0
  7. package/api-doc/{event.event.json → log.json} +96 -101
  8. package/api-doc/narrator.json +647 -0
  9. package/api-doc/proto.json +995 -0
  10. package/api-doc/saver.saver.json +713 -263
  11. package/api-doc/saver.storage.json +282 -52
  12. package/api-doc/squid.json +846 -0
  13. package/api-doc/tweener.json +277 -0
  14. package/generated/bridge.d.ts +468 -0
  15. package/generated/decore.d.ts +36 -36
  16. package/generated/druid.d.ts +137 -443
  17. package/generated/event.d.ts +318 -0
  18. package/generated/immutable.d.ts +13 -0
  19. package/generated/lang.d.ts +101 -0
  20. package/generated/log.d.ts +36 -0
  21. package/generated/narrator.d.ts +121 -0
  22. package/generated/proto.d.ts +146 -0
  23. package/generated/saver.saver.d.ts +287 -42
  24. package/generated/saver.storage.d.ts +77 -14
  25. package/generated/squid.d.ts +127 -0
  26. package/generated/tweener.d.ts +42 -0
  27. package/library-classification.json +0 -71
  28. package/library-targets.json +0 -66
  29. package/luals-targets.json +114 -0
  30. package/package.json +4 -33
  31. package/script-api-targets.json +15 -0
  32. package/scripts/__snapshots__/parse-luals.test.ts.snap +340 -75
  33. package/scripts/apply-luals-overrides.ts +63 -0
  34. package/scripts/emit-library-dts.ts +84 -3
  35. package/scripts/lower-api-doc.ts +48 -16
  36. package/scripts/luals-fidelity.ts +7 -0
  37. package/scripts/map-luals-types.ts +34 -1
  38. package/scripts/parse-luals.ts +423 -18
  39. package/scripts/sync-luals-types.ts +15 -1
  40. package/scripts/sync-script-api-types.ts +368 -0
  41. package/api-doc/immutable.immutable.json +0 -63
  42. package/api-doc/lang.lang.json +0 -411
  43. package/api-doc/log.log.json +0 -50
  44. package/api-doc/narrator.narrator.json +0 -150
  45. package/api-doc/proto.proto.json +0 -355
  46. package/api-doc/squid.squid.json +0 -660
  47. package/api-doc/tweener.tweener.json +0 -419
  48. package/generated/bridge.bridge.d.ts +0 -533
  49. package/generated/event.event.d.ts +0 -54
  50. package/generated/immutable.immutable.d.ts +0 -13
  51. package/generated/lang.lang.d.ts +0 -33
  52. package/generated/log.log.d.ts +0 -40
  53. package/generated/narrator.narrator.d.ts +0 -66
  54. package/generated/proto.proto.d.ts +0 -36
  55. package/generated/squid.squid.d.ts +0 -106
  56. package/generated/tweener.tweener.d.ts +0 -151
@@ -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,16 @@ 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
+ }
39
+
40
+ export interface LibraryOverload {
41
+ type: string;
42
+ doc: string;
28
43
  }
29
44
 
30
45
  export interface LibraryMethod {
@@ -33,6 +48,11 @@ export interface LibraryMethod {
33
48
  generics: LibraryGeneric[];
34
49
  params: LibraryParam[];
35
50
  returns: LibraryParam[];
51
+ // A standalone `---@local`/`---@private`/`---@protected`/`---@package` before the
52
+ // function declaration. Set only when marked (like a field's `visibility`), so an
53
+ // unmarked method carries no key. `local` has no `@field` analogue (LuaLS field
54
+ // scope has no `local`), so a method's visibility widens the field set with it.
55
+ visibility?: LibraryMethodVisibility;
36
56
  }
37
57
 
38
58
  export interface LibraryParam {
@@ -41,10 +61,17 @@ export interface LibraryParam {
41
61
  doc: string;
42
62
  isOptional: boolean;
43
63
  isVararg: boolean;
64
+ // True when the raw type token carries a top-level `nil` union member (`T|nil`),
65
+ // distinct from the literal trailing `?` that drives `isOptional`. Set only when
66
+ // true (like a field's `visibility`), so a non-nil-bearing param carries no key.
67
+ // The emitter's trailing-run rule treats `isOptional || isNilable` as omittable.
68
+ isNilable?: boolean;
44
69
  }
45
70
 
46
71
  export type LibraryFieldVisibility = "public" | "protected" | "private" | "package";
47
72
 
73
+ export type LibraryMethodVisibility = LibraryFieldVisibility | "local";
74
+
48
75
  export interface LibraryField {
49
76
  name: string;
50
77
  types: string[];
@@ -69,9 +96,17 @@ interface Pending {
69
96
  params: LibraryParam[];
70
97
  returns: LibraryParam[];
71
98
  generics: LibraryGeneric[];
99
+ overloads: LibraryOverload[];
100
+ visibility?: LibraryMethodVisibility;
72
101
  }
73
102
 
74
- const emptyPending = (): Pending => ({ doc: [], params: [], returns: [], generics: [] });
103
+ const emptyPending = (): Pending => ({
104
+ doc: [],
105
+ params: [],
106
+ returns: [],
107
+ generics: [],
108
+ overloads: [],
109
+ });
75
110
 
76
111
  /**
77
112
  * Read a single raw type token from the head of `rest`, honoring bracket depth so
@@ -101,6 +136,170 @@ function readTypeToken(rest: string): { type: string; rest: string } {
101
136
  return { type: rest.slice(0, i), rest: rest.slice(i).trim() };
102
137
  }
103
138
 
139
+ /** Index of the bracket matching the opener at `open`, or -1 if unbalanced. */
140
+ function matchCloser(s: string, open: number): number {
141
+ const pairs: Record<string, string> = { "<": ">", "(": ")", "[": "]", "{": "}" };
142
+ const want = pairs[s[open] as string];
143
+ let depth = 0;
144
+ let inQuote = false;
145
+ for (let i = open; i < s.length; i++) {
146
+ const c = s[i];
147
+ if (inQuote) {
148
+ if (c === '"') inQuote = false;
149
+ continue;
150
+ }
151
+ if (c === '"') inQuote = true;
152
+ else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
153
+ else if (c === ">" || c === ")" || c === "]" || c === "}") {
154
+ depth--;
155
+ if (depth === 0) return c === want ? i : -1;
156
+ }
157
+ }
158
+ return -1;
159
+ }
160
+
161
+ /**
162
+ * Split `s` on every top-level occurrence of the single-character `sep`, honoring
163
+ * bracket depth and double-quoted string literals so a separator nested inside
164
+ * `<...>`, `(...)`, `[...]`, `{...}`, or a `"..."` literal does not split. A parser-
165
+ * local copy of the mapper's identical helper — the parser is upstream of the mapper
166
+ * and must not import it (`hasTopLevelNil`'s comment).
167
+ */
168
+ function splitTopLevel(s: string, sep: string): string[] {
169
+ const parts: string[] = [];
170
+ let depth = 0;
171
+ let inQuote = false;
172
+ let start = 0;
173
+ for (let i = 0; i < s.length; i++) {
174
+ const c = s[i];
175
+ if (inQuote) {
176
+ if (c === '"') inQuote = false;
177
+ continue;
178
+ }
179
+ if (c === '"') inQuote = true;
180
+ else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
181
+ else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
182
+ else if (depth === 0 && c === sep) {
183
+ parts.push(s.slice(start, i));
184
+ start = i + 1;
185
+ }
186
+ }
187
+ parts.push(s.slice(start));
188
+ return parts;
189
+ }
190
+
191
+ /**
192
+ * Decompose a `fun(...)` token into a method's `params`/`returns`, dropping a leading
193
+ * `self`. Used for a function-local `---@class` member typed by `---@type fun(...)`:
194
+ * modeling it as a method (each param keeping its raw type token) rather than a field
195
+ * makes the emitter render a full-typed method instead of the lossy permissive hook
196
+ * `matchSelfHookField` would produce. An unbalanced token yields empty lists.
197
+ */
198
+ function funToMethodParts(token: string): { params: LibraryParam[]; returns: LibraryParam[] } {
199
+ const open = token.indexOf("(");
200
+ const close = open === -1 ? -1 : matchCloser(token, open);
201
+ if (open === -1 || close === -1) return { params: [], returns: [] };
202
+ const paramsStr = token.slice(open + 1, close).trim();
203
+ const afterClose = token.slice(close + 1).trim();
204
+
205
+ const params: LibraryParam[] = [];
206
+ const rawParams = paramsStr === "" ? [] : splitTopLevel(paramsStr, ",");
207
+ for (const rawPart of rawParams) {
208
+ const part = rawPart.trim();
209
+ if (part === "") continue;
210
+ if (part.startsWith("...")) {
211
+ const after = part.slice(3).trim();
212
+ const type = after.startsWith(":") ? after.slice(1).trim() : "";
213
+ const vararg: LibraryParam = {
214
+ name: "...",
215
+ types: type ? [type] : [],
216
+ doc: "",
217
+ isOptional: false,
218
+ isVararg: true,
219
+ };
220
+ if (type && hasTopLevelNil(type)) vararg.isNilable = true;
221
+ params.push(vararg);
222
+ continue;
223
+ }
224
+ const colon = splitTopLevel(part, ":");
225
+ const rawName = (colon[0] ?? "").trim();
226
+ const typeExpr = colon.length >= 2 ? colon.slice(1).join(":").trim() : "";
227
+ const isOptional = rawName.endsWith("?");
228
+ const name = isOptional ? rawName.slice(0, -1) : rawName;
229
+ const param: LibraryParam = {
230
+ name,
231
+ types: typeExpr ? [typeExpr] : [],
232
+ doc: "",
233
+ isOptional,
234
+ isVararg: false,
235
+ };
236
+ if (typeExpr && hasTopLevelNil(typeExpr)) param.isNilable = true;
237
+ params.push(param);
238
+ }
239
+ if (params[0]?.name === "self") params.shift();
240
+
241
+ const returns: LibraryParam[] = [];
242
+ if (afterClose.startsWith(":")) {
243
+ const retStr = afterClose.slice(1).trim();
244
+ for (const raw of retStr === "" ? [] : splitTopLevel(retStr, ",")) {
245
+ const type = raw.trim();
246
+ if (type)
247
+ returns.push({ name: "", types: [type], doc: "", isOptional: false, isVararg: false });
248
+ }
249
+ }
250
+ return { params, returns };
251
+ }
252
+
253
+ /**
254
+ * True when the raw type token has a top-level `nil` union member (`T|nil`,
255
+ * `fun()|nil`) — the signal that a parameter is nil-bearing and can be emitted
256
+ * TS-optional. Bracket- and quote-depth aware so a `nil` nested in
257
+ * `table<...>`/`{...}`/a `"..."` literal does not count, and a `fun(...): ret|nil`
258
+ * return-union (whose `|nil` sits at depth 0 after the `)`) is recognized as the
259
+ * function's own return, not an outer nullable — only a `|nil` applied to the whole
260
+ * token flags the param. Self-contained: the parser is upstream of the mapper and
261
+ * must not import it.
262
+ */
263
+ function hasTopLevelNil(rawToken: string): boolean {
264
+ let token = rawToken.trim();
265
+ while (token.startsWith("(") && matchCloser(token, 0) === token.length - 1) {
266
+ token = token.slice(1, -1).trim();
267
+ }
268
+ if (/^fun\s*\(/.test(token)) {
269
+ const close = matchCloser(token, token.indexOf("("));
270
+ if (
271
+ close !== -1 &&
272
+ token
273
+ .slice(close + 1)
274
+ .trim()
275
+ .startsWith(":")
276
+ )
277
+ return false;
278
+ }
279
+ // A type-suffix `T?` (`string?`, `fun()?`, `(a|b)?`) is nil-bearing. Placed after the
280
+ // fun-return guard so a nullable return (`fun(): a|nil`, `fun(): string?`) never flags.
281
+ if (token.endsWith("?")) return true;
282
+ const isNilSeg = (from: number, to: number): boolean => token.slice(from, to).trim() === "nil";
283
+ let depth = 0;
284
+ let inQuote = false;
285
+ let segStart = 0;
286
+ for (let i = 0; i < token.length; i++) {
287
+ const c = token[i];
288
+ if (inQuote) {
289
+ if (c === '"') inQuote = false;
290
+ continue;
291
+ }
292
+ if (c === '"') inQuote = true;
293
+ else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
294
+ else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
295
+ else if (depth === 0 && c === "|") {
296
+ if (isNilSeg(segStart, i)) return true;
297
+ segStart = i + 1;
298
+ }
299
+ }
300
+ return isNilSeg(segStart, token.length);
301
+ }
302
+
104
303
  /** A bare lowercase identifier — the shape druid uses for an optional `@return` name. */
105
304
  const RETURN_NAME = /^[a-z_][A-Za-z0-9_]*$/;
106
305
 
@@ -112,7 +311,9 @@ function parseParam(rest: string): LibraryParam {
112
311
  const isOptional = !isVararg && rawName.endsWith("?");
113
312
  const name = isOptional ? rawName.slice(0, -1) : rawName;
114
313
  const { type, rest: doc } = readTypeToken(afterName);
115
- return { name, types: type ? [type] : [], doc, isOptional, isVararg };
314
+ const param: LibraryParam = { name, types: type ? [type] : [], doc, isOptional, isVararg };
315
+ if (type && hasTopLevelNil(type)) param.isNilable = true;
316
+ return param;
116
317
  }
117
318
 
118
319
  function parseReturn(rest: string): LibraryParam {
@@ -152,9 +353,12 @@ function parseField(rest: string): LibraryField {
152
353
  const spaceAt = body.search(/\s/);
153
354
  const rawName = spaceAt === -1 ? body : body.slice(0, spaceAt);
154
355
  const afterName = spaceAt === -1 ? "" : body.slice(spaceAt).trim();
155
- const isOptional = rawName.endsWith("?");
156
- const name = isOptional ? rawName.slice(0, -1) : rawName;
356
+ const nameSuffix = rawName.endsWith("?");
357
+ const name = nameSuffix ? rawName.slice(0, -1) : rawName;
157
358
  const { type, rest: doc } = readTypeToken(afterName);
359
+ // Interface properties carry no optional-before-required rule, so a `|nil`/`?` type
360
+ // folds straight into the field's optionality.
361
+ const isOptional = nameSuffix || (type !== "" && hasTopLevelNil(type));
158
362
  return {
159
363
  name,
160
364
  types: type ? [type] : [],
@@ -166,7 +370,15 @@ function parseField(rest: string): LibraryField {
166
370
 
167
371
  function parseVararg(rest: string): LibraryParam {
168
372
  const { type, rest: doc } = readTypeToken(rest);
169
- return { name: "...", types: type ? [type] : [], doc, isOptional: false, isVararg: true };
373
+ const param: LibraryParam = {
374
+ name: "...",
375
+ types: type ? [type] : [],
376
+ doc,
377
+ isOptional: false,
378
+ isVararg: true,
379
+ };
380
+ if (type && hasTopLevelNil(type)) param.isNilable = true;
381
+ return param;
170
382
  }
171
383
 
172
384
  function parseGenerics(rest: string): LibraryGeneric[] {
@@ -181,12 +393,50 @@ function parseGenerics(rest: string): LibraryGeneric[] {
181
393
  });
182
394
  }
183
395
 
184
- /** Parse a `@class Name[ : parent]` head. The parent is kept as a single raw token. */
396
+ /**
397
+ * Read one class-head token from `s` starting at `start`, tracking `<[({` depth so a
398
+ * generic like `Bar<A, B>` stays whole. Ends at the first top-level space/tab, `:`, or
399
+ * `,` (none consumed). Returns the trimmed token and its end index. Distinct from
400
+ * `readTypeToken`, whose `fun(...)` `:`/`,` continuation rule wrongly glues a
401
+ * `Name: Parent` head together.
402
+ */
403
+ function readClassToken(s: string, start: number): { token: string; end: number } {
404
+ let depth = 0;
405
+ let i = start;
406
+ for (; i < s.length; i++) {
407
+ const c = s[i];
408
+ if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
409
+ else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
410
+ else if (depth === 0 && (c === " " || c === "\t" || c === ":" || c === ",")) break;
411
+ }
412
+ return { token: s.slice(start, i).trim(), end: i };
413
+ }
414
+
415
+ /**
416
+ * Parse a `@class Name[: parent[, parent...]] [description]` head. The identifier and
417
+ * optional parent list are read as bracket-aware tokens; any trailing human
418
+ * description is dropped. Multiple parents are joined with `", "` to match the single
419
+ * `extends` string the emitter renders.
420
+ */
185
421
  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 } : {}) };
422
+ const s = rest.trim();
423
+ const { token: name, end: afterName } = readClassToken(s, 0);
424
+ let i = afterName;
425
+ while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
426
+ if (s[i] !== ":") return { name };
427
+ i++;
428
+ const parents: string[] = [];
429
+ while (true) {
430
+ while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
431
+ const { token, end } = readClassToken(s, i);
432
+ if (token !== "") parents.push(token);
433
+ i = end;
434
+ while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
435
+ if (s[i] !== ",") break;
436
+ i++;
437
+ }
438
+ const extendsStr = parents.join(", ");
439
+ return { name, ...(extendsStr ? { extends: extendsStr } : {}) };
190
440
  }
191
441
 
192
442
  interface FunctionDecl {
@@ -260,6 +510,19 @@ export function parseLualsSource(source: string): LibraryModel {
260
510
  let pending = emptyPending();
261
511
  let openClass: LibraryInterface | null = null;
262
512
  let lastOpenedClass: string | null = null;
513
+ let moduleObject: string | undefined;
514
+ let lastModuleFunction: LibraryMethod | null = null;
515
+ // The one open function-local `---@class` block (squid's `SquidInstance` inside
516
+ // `Squid.new`). `owner` is the function whose body it sits in, so a returned local
517
+ // bound to it infers the function's return; `pendingType` is a `---@type fun(...)`
518
+ // armed for the next member key.
519
+ let localClass: {
520
+ iface: LibraryInterface;
521
+ localVar: string | null;
522
+ owner: LibraryMethod | null;
523
+ pendingType: string | null;
524
+ returnedSelf: boolean;
525
+ } | null = null;
263
526
 
264
527
  const ensureInterface = (name: string): LibraryInterface => {
265
528
  const existing = byName.get(name);
@@ -282,11 +545,93 @@ export function parseLualsSource(source: string): LibraryModel {
282
545
  generics: pending.generics,
283
546
  params: pending.params,
284
547
  returns: pending.returns,
548
+ ...(pending.visibility ? { visibility: pending.visibility } : {}),
285
549
  });
286
550
 
551
+ // Interpret one indented line while a function-local `---@class` block is open. Only
552
+ // the `---@type fun(...)` + next `<key> = ...` member pattern, the backing `local`,
553
+ // and the `return <local>` are recognized; every other indented line stays opaque.
554
+ const handleLocalClassLine = (line: string): void => {
555
+ const lc = localClass;
556
+ if (!lc) return;
557
+ const typeMatch = /^---@type\s+(.+)$/.exec(line);
558
+ if (typeMatch) {
559
+ const { type } = readTypeToken(typeMatch[1] ?? "");
560
+ // A non-`fun` `---@type` also disarms a previously-armed one.
561
+ lc.pendingType = /^fun\s*\(/.test(type) ? type : null;
562
+ return;
563
+ }
564
+ if (lc.pendingType) {
565
+ const keyMatch = /^([A-Za-z_]\w*)\s*=/.exec(line);
566
+ if (keyMatch) {
567
+ const { params, returns } = funToMethodParts(lc.pendingType);
568
+ lc.iface.methods.push({
569
+ name: keyMatch[1] as string,
570
+ brief: "",
571
+ generics: [],
572
+ params,
573
+ returns,
574
+ });
575
+ lc.pendingType = null;
576
+ return;
577
+ }
578
+ // Not the immediately-following member key: the armed type has no member.
579
+ lc.pendingType = null;
580
+ }
581
+ const returnMatch = /^return\s+([A-Za-z_]\w*)\s*$/.exec(line);
582
+ if (returnMatch) {
583
+ if (returnMatch[1] === lc.localVar) lc.returnedSelf = true;
584
+ return;
585
+ }
586
+ const local = LOCAL_ASSIGN.exec(line);
587
+ if (local) {
588
+ // A `---@class` annotates the immediately-following declaration, so only the
589
+ // first local after the class opens is the instance; a later local must not rebind.
590
+ if (lc.localVar === null) lc.localVar = local[1] ?? null;
591
+ return;
592
+ }
593
+ };
594
+
287
595
  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;
596
+ const indented = /^\s/.test(raw);
597
+ if (indented || raw.length === 0) {
598
+ if (localClass) {
599
+ handleLocalClassLine(raw.trim());
600
+ } else if (indented) {
601
+ // Only an indented `---@class` opens function-local capture; every other
602
+ // indented line (druid-style `---@cast`/`---@type` narrowing) stays opaque.
603
+ const classMatch = /^---@class\s+(.+)$/.exec(raw.trim());
604
+ if (classMatch) {
605
+ const head = parseClassHead(classMatch[1] ?? "");
606
+ const iface = ensureInterface(head.name);
607
+ if (head.extends) iface.extends = head.extends;
608
+ localClass = {
609
+ iface,
610
+ localVar: null,
611
+ owner: lastModuleFunction,
612
+ pendingType: null,
613
+ returnedSelf: false,
614
+ };
615
+ }
616
+ }
617
+ continue;
618
+ }
619
+
620
+ // A column-0 line ends any open function-local class (dedent). Apply the inferred
621
+ // return to the owner only when it declared no explicit `---@return`.
622
+ if (localClass) {
623
+ const lc = localClass;
624
+ if (lc.returnedSelf && lc.owner && lc.owner.returns.length === 0) {
625
+ lc.owner.returns.push({
626
+ name: "",
627
+ types: [lc.iface.name],
628
+ doc: "",
629
+ isOptional: false,
630
+ isVararg: false,
631
+ });
632
+ }
633
+ localClass = null;
634
+ }
290
635
 
291
636
  if (raw.startsWith("---@")) {
292
637
  const tagMatch = /^---@([a-zA-Z]+)\s*(.*)$/.exec(raw);
@@ -300,6 +645,7 @@ export function parseLualsSource(source: string): LibraryModel {
300
645
  if (head.extends) iface.extends = head.extends;
301
646
  if (pending.doc.length > 0 && iface.brief === "") iface.brief = pending.doc.join("\n");
302
647
  if (pending.generics.length > 0) iface.generics = pending.generics;
648
+ if (pending.overloads.length > 0) iface.overloads = pending.overloads;
303
649
  openClass = iface;
304
650
  lastOpenedClass = head.name;
305
651
  pending = emptyPending();
@@ -325,6 +671,15 @@ export function parseLualsSource(source: string): LibraryModel {
325
671
  pending.generics.push(...parseGenerics(rest));
326
672
  break;
327
673
  }
674
+ case "overload": {
675
+ // A class-level `---@overload fun(...)`: keep the raw `fun(...)` token via
676
+ // readTypeToken (its spaced `): ret` return stays whole) plus the trailing
677
+ // doc, and transfer to the interface on the following `@class` — like brief
678
+ // and generics. A non-`fun` overload is outside the modeled subset and dropped.
679
+ const { type, rest: doc } = readTypeToken(rest);
680
+ if (/^fun\s*\(/.test(type)) pending.overloads.push({ type, doc });
681
+ break;
682
+ }
328
683
  case "alias": {
329
684
  const spaceAt = rest.search(/\s/);
330
685
  const name = spaceAt === -1 ? rest : rest.slice(0, spaceAt);
@@ -333,9 +688,19 @@ export function parseLualsSource(source: string): LibraryModel {
333
688
  pending = emptyPending();
334
689
  break;
335
690
  }
691
+ case "private":
692
+ case "protected":
693
+ case "package":
694
+ case "local": {
695
+ // A standalone visibility tag on the pending block marks the next function
696
+ // declaration's method/module-function `visibility` (a `@class` or `@alias`
697
+ // that consumes the block first resets it, so it never leaks onto a type).
698
+ pending.visibility = tag as LibraryMethodVisibility;
699
+ break;
700
+ }
336
701
  default:
337
- // @private, @protected, @cast, @type, @diagnostic, @overload, ... — outside
338
- // the Druid subset; recognized as a tag and skipped, never treated as doc.
702
+ // @cast, @type, @diagnostic, ... — outside the Druid subset; recognized as a
703
+ // tag and skipped, never treated as doc.
339
704
  break;
340
705
  }
341
706
  continue;
@@ -348,12 +713,16 @@ export function parseLualsSource(source: string): LibraryModel {
348
713
 
349
714
  const decl = parseFunctionDecl(raw);
350
715
  if (decl) {
716
+ const method = methodFromPending(decl.name);
351
717
  if (decl.kind === "method") {
352
718
  const target = decl.receiver ? (receiverBinding.get(decl.receiver) ?? decl.receiver) : "";
353
- ensureInterface(target).methods.push(methodFromPending(decl.name));
719
+ ensureInterface(target).methods.push(method);
354
720
  } else if (decl.qualified) {
355
- moduleFunctions.push(methodFromPending(decl.name));
721
+ moduleFunctions.push(method);
356
722
  }
723
+ // Any function may host a function-local `---@class`; remember the enclosing
724
+ // function so a returned local can infer its return, even a bare helper.
725
+ lastModuleFunction = method;
357
726
  pending = emptyPending();
358
727
  openClass = null;
359
728
  continue;
@@ -366,10 +735,34 @@ export function parseLualsSource(source: string): LibraryModel {
366
735
  lastOpenedClass = null;
367
736
  openClass = null;
368
737
  pending = emptyPending();
738
+ continue;
739
+ }
740
+
741
+ // A column-0 `return <name>` handing back a local bound to an opened `---@class`
742
+ // marks that class as the module object (its fields become module-level consts).
743
+ // Restricted to squid's constants-table idiom to keep every other library's golden
744
+ // untouched: the returned local is named after its class (`local Squid = {}` /
745
+ // `return Squid`, not a generic `local M` alias), the class carries at least one
746
+ // public field, and it has no methods. A component/instance class (colon methods),
747
+ // an opaque handle (no fields), or an `M`-aliased module table is a type consumers
748
+ // reference or a plain namespace, so it stays a standalone interface.
749
+ const returnStmt = /^return\s+([A-Za-z_][\w.]*)\s*$/.exec(raw);
750
+ if (returnStmt) {
751
+ const name = returnStmt[1] as string;
752
+ const resolvedName = receiverBinding.get(name) ?? (byName.has(name) ? name : undefined);
753
+ const resolved = resolvedName ? byName.get(resolvedName) : undefined;
754
+ if (
755
+ resolved &&
756
+ resolvedName === name &&
757
+ resolved.methods.length === 0 &&
758
+ resolved.fields.some((f) => f.visibility === undefined || f.visibility === "public")
759
+ ) {
760
+ moduleObject = resolved.name;
761
+ }
369
762
  }
370
763
  }
371
764
 
372
- return { interfaces, aliases, moduleFunctions };
765
+ return { interfaces, aliases, moduleFunctions, ...(moduleObject ? { moduleObject } : {}) };
373
766
  }
374
767
 
375
768
  /**
@@ -383,8 +776,10 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
383
776
  const byName = new Map<string, LibraryInterface>();
384
777
  const aliases: LibraryAlias[] = [];
385
778
  const moduleFunctions: LibraryMethod[] = [];
779
+ let moduleObject: string | undefined;
386
780
 
387
781
  for (const model of models) {
782
+ if (!moduleObject && model.moduleObject) moduleObject = model.moduleObject;
388
783
  for (const iface of model.interfaces) {
389
784
  const existing = byName.get(iface.name);
390
785
  if (!existing) {
@@ -395,6 +790,9 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
395
790
  fields: [...iface.fields],
396
791
  methods: [...iface.methods],
397
792
  brief: iface.brief,
793
+ ...(iface.overloads && iface.overloads.length > 0
794
+ ? { overloads: [...iface.overloads] }
795
+ : {}),
398
796
  };
399
797
  byName.set(iface.name, copy);
400
798
  interfaces.push(copy);
@@ -407,6 +805,13 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
407
805
  if (existing.generics.length === 0 && iface.generics.length > 0) {
408
806
  existing.generics = [...iface.generics];
409
807
  }
808
+ if (
809
+ (!existing.overloads || existing.overloads.length === 0) &&
810
+ iface.overloads &&
811
+ iface.overloads.length > 0
812
+ ) {
813
+ existing.overloads = [...iface.overloads];
814
+ }
410
815
  }
411
816
  aliases.push(...model.aliases);
412
817
  moduleFunctions.push(...model.moduleFunctions);
@@ -419,7 +824,7 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
419
824
  // overloaded module functions keep every signature.
420
825
  for (const iface of interfaces) iface.fields = dedupeByName(iface.fields);
421
826
 
422
- return { interfaces, aliases, moduleFunctions };
827
+ return { interfaces, aliases, moduleFunctions, ...(moduleObject ? { moduleObject } : {}) };
423
828
  }
424
829
 
425
830
  function dedupeByName<T extends { name: string }>(items: T[]): T[] {
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { type AnnotationOverrides, applyAnnotationOverrides } from "./apply-luals-overrides";
3
4
  import { emitLibraryDeclarations } from "./emit-library-dts";
4
5
  import { lowerLibraryModel } from "./lower-api-doc";
5
6
  import { buildFidelityReport, type FidelityReport } from "./luals-fidelity";
@@ -22,6 +23,10 @@ export interface LualsTarget {
22
23
  // SPDX-style license id, surfaced by the docs-site provenance block. Optional
23
24
  // in the config; the docs-site defaults an absent value to "".
24
25
  license?: string;
26
+ // Post-merge corrections applied in `buildTargetModel` for the cases where an
27
+ // upstream annotation the fixtures freeze diverges from the runtime. Optional and
28
+ // loud-failing on an absent key — see `apply-luals-overrides.ts`.
29
+ annotationOverrides?: AnnotationOverrides;
25
30
  }
26
31
 
27
32
  export interface LualsTargets {
@@ -57,6 +62,9 @@ export function readLualsTargets(packageRoot: string): LualsTarget[] {
57
62
  typeRenames: entry.typeRenames ?? {},
58
63
  ignore: entry.ignore ?? [],
59
64
  ...(entry.license !== undefined ? { license: entry.license } : {}),
65
+ ...(entry.annotationOverrides !== undefined
66
+ ? { annotationOverrides: entry.annotationOverrides }
67
+ : {}),
60
68
  };
61
69
  });
62
70
  }
@@ -188,7 +196,13 @@ export function buildTargetModel(
188
196
  const parsed = new Map<string, LibraryModel>();
189
197
  for (const rel of files) parsed.set(rel, parseLualsSource(readFileSync(join(root, rel), "utf8")));
190
198
  const merged = mergeLibraryModels([...parsed.values()]);
191
- return { ...merged, moduleFunctions: parsed.get(ownFile)?.moduleFunctions ?? [] };
199
+ const model: LibraryModel = {
200
+ ...merged,
201
+ moduleFunctions: parsed.get(ownFile)?.moduleFunctions ?? [],
202
+ };
203
+ return target.annotationOverrides
204
+ ? applyAnnotationOverrides(model, target.annotationOverrides)
205
+ : model;
192
206
  }
193
207
 
194
208
  export function buildTargetFidelity(packageRoot: string, target: LualsTarget): FidelityReport {