@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
@@ -15,34 +15,37 @@
15
15
 
16
16
  import {
17
17
  buildModelContext,
18
+ type ExternalTypeRef,
19
+ isPublicField,
20
+ isPublicMethod,
18
21
  mapTypes,
22
+ paramOptionalFlags,
19
23
  renderGenericParams,
20
24
  sanitizeTypeName,
21
25
  } from "./emit-library-dts";
22
26
  import { type MapContext, scopeGenerics } from "./map-luals-types";
23
27
  import type { LibraryField, LibraryMethod, LibraryModel, LibraryParam } from "./parse-luals";
24
28
 
25
- // A field with an explicit non-public visibility is internal surface; keep only
26
- // fields with no visibility or an explicit `public`, mirroring how LuaLS hides
27
- // `private`/`protected`/`package` members from a class's public shape.
28
- function isPublicField(field: LibraryField): boolean {
29
- return field.visibility === undefined || field.visibility === "public";
30
- }
31
-
32
29
  // Each type token is mapped independently (one mapped TS string per token) so the
33
30
  // ref-doc `types` array stays token-per-slot the way engine ref-docs are shaped.
34
31
  function mapTokens(tokens: readonly string[], ctx: MapContext): string[] {
35
32
  return tokens.map((token) => mapTypes([token], ctx));
36
33
  }
37
34
 
38
- function parameterElement(param: LibraryParam, ctx: MapContext): Record<string, unknown> {
35
+ function parameterElement(
36
+ param: LibraryParam,
37
+ isOptional: boolean,
38
+ ctx: MapContext,
39
+ ): Record<string, unknown> {
39
40
  // A vararg's element type stays a plain mapped token here; the renderer arrayifies
40
41
  // it (`...args: T[]`) from the `is_vararg` flag, keeping the JSON structurally honest.
42
+ // `isOptional` comes from the emitter's trailing-run rule (paramOptionalFlags), not
43
+ // `param.isOptional` alone, so the `/api` signature matches the emitted `.d.ts`.
41
44
  return {
42
45
  name: param.isVararg ? "...args" : param.name,
43
46
  doc: param.doc,
44
47
  types: mapTokens(param.types, ctx),
45
- is_optional: param.isOptional ? "True" : "False",
48
+ is_optional: isOptional ? "True" : "False",
46
49
  is_vararg: param.isVararg ? "True" : "False",
47
50
  };
48
51
  }
@@ -54,17 +57,24 @@ function returnElement(ret: LibraryParam, ctx: MapContext): Record<string, unkno
54
57
  function functionElement(method: LibraryMethod, ctx: MapContext): Record<string, unknown> {
55
58
  const fnCtx = scopeGenerics(ctx, method.generics);
56
59
  const generics = renderGenericParams(method.generics, fnCtx);
60
+ const optionalFlags = paramOptionalFlags(method.params);
57
61
  return {
58
62
  type: "FUNCTION",
59
63
  name: method.name,
60
64
  brief: method.brief,
61
65
  description: method.brief,
62
66
  ...(generics !== "" ? { generics } : {}),
63
- parameters: method.params.map((param) => parameterElement(param, fnCtx)),
67
+ parameters: method.params.map((param, index) =>
68
+ parameterElement(param, optionalFlags[index] ?? false, fnCtx),
69
+ ),
64
70
  returnvalues: method.returns.map((ret) => returnElement(ret, fnCtx)),
71
+ ...(method.deprecated !== undefined ? { deprecated: method.deprecated } : {}),
65
72
  };
66
73
  }
67
74
 
75
+ // No `deprecated` spread here or on `variableElement`: both take a `LibraryField`,
76
+ // and a one-line `---@field` has no slot for a block tag in this dialect, so a
77
+ // property can never carry one.
68
78
  function propertyElement(field: LibraryField, ctx: MapContext): Record<string, unknown> {
69
79
  return {
70
80
  name: field.name,
@@ -74,20 +84,52 @@ function propertyElement(field: LibraryField, ctx: MapContext): Record<string, u
74
84
  };
75
85
  }
76
86
 
87
+ // A module object's field lowered as a top-level `VARIABLE` — the ref-doc shape for a
88
+ // module constant (`squid.TRACE`), matching the emitter's module-level `export const`.
89
+ function variableElement(field: LibraryField, ctx: MapContext): Record<string, unknown> {
90
+ return {
91
+ type: "VARIABLE",
92
+ name: field.name,
93
+ brief: field.doc,
94
+ description: field.doc,
95
+ types: mapTokens(field.types, ctx),
96
+ };
97
+ }
98
+
77
99
  export function lowerLibraryModel(
78
100
  model: LibraryModel,
79
- { namespace, typeRenames }: { namespace: string; typeRenames?: Record<string, string> },
101
+ {
102
+ namespace,
103
+ typeRenames,
104
+ externalTypes,
105
+ }: {
106
+ namespace: string;
107
+ typeRenames?: Record<string, string>;
108
+ externalTypes?: Record<string, ExternalTypeRef> | undefined;
109
+ },
80
110
  ): unknown {
81
- const ctx = buildModelContext(model, typeRenames);
111
+ const ctx = buildModelContext(model, typeRenames, externalTypes);
82
112
  const elements: Record<string, unknown>[] = [];
83
113
 
84
114
  for (const fn of model.moduleFunctions) {
115
+ if (!isPublicMethod(fn)) continue;
85
116
  elements.push(functionElement(fn, ctx));
86
117
  }
87
118
 
88
119
  for (const iface of model.interfaces) {
89
120
  const ifaceCtx = scopeGenerics(ctx, iface.generics);
90
- const functions = iface.methods.map((method) => functionElement(method, ifaceCtx));
121
+ // The module object's public fields are module-level constants, lowered as top-level
122
+ // VARIABLE elements rather than a TYPEDEF named after the class.
123
+ if (iface.name === model.moduleObject) {
124
+ for (const field of iface.fields) {
125
+ if (!isPublicField(field)) continue;
126
+ elements.push(variableElement(field, ifaceCtx));
127
+ }
128
+ continue;
129
+ }
130
+ const functions = iface.methods
131
+ .filter(isPublicMethod)
132
+ .map((method) => functionElement(method, ifaceCtx));
91
133
  const properties = iface.fields
92
134
  .filter(isPublicField)
93
135
  .map((field) => propertyElement(field, ifaceCtx));
@@ -96,18 +138,26 @@ export function lowerLibraryModel(
96
138
  name: sanitizeTypeName(iface.name),
97
139
  ...(functions.length > 0 ? { functions } : {}),
98
140
  ...(properties.length > 0 ? { properties } : {}),
141
+ ...(iface.deprecated !== undefined ? { deprecated: iface.deprecated } : {}),
99
142
  });
100
143
  }
101
144
 
102
145
  for (const alias of model.aliases) {
103
- elements.push({ type: "TYPEDEF", name: sanitizeTypeName(alias.name) });
146
+ elements.push({
147
+ type: "TYPEDEF",
148
+ name: sanitizeTypeName(alias.name),
149
+ ...(alias.deprecated !== undefined ? { deprecated: alias.deprecated } : {}),
150
+ });
104
151
  }
105
152
 
106
- // The module's own `@class` (named for the namespace, e.g. `@class druid`)
107
- // carries the library's summary; use it as the page description so a
108
- // LuaLS-sourced library reads with an intro like every other `/api` page,
109
- // rather than opening on a bare provenance block. `brief` is its first line.
110
- const moduleClass = model.interfaces.find((iface) => iface.name === namespace);
153
+ // The module's own `@class` carries the library's summary; use it as the page
154
+ // description so a LuaLS-sourced library reads with an intro like every other `/api`
155
+ // page, rather than opening on a bare provenance block. `brief` is its first line.
156
+ // A tracked `moduleObject` names it directly (squid's `Squid` != namespace `squid`);
157
+ // otherwise fall back to the class named for the namespace (`@class druid`).
158
+ const moduleClass = model.interfaces.find(
159
+ (iface) => iface.name === (model.moduleObject ?? namespace),
160
+ );
111
161
  const description = moduleClass?.brief ?? "";
112
162
  const brief = description.split("\n")[0] ?? "";
113
163
 
@@ -10,6 +10,12 @@
10
10
  * sorted-unique token list so the gap is visible instead of silent.
11
11
  */
12
12
 
13
+ import {
14
+ buildModelContext,
15
+ type ExternalTypeRef,
16
+ isPublicField,
17
+ isPublicMethod,
18
+ } from "./emit-library-dts";
13
19
  import { type MapContext, mapLualsType, scopeGenerics } from "./map-luals-types";
14
20
  import type { LibraryGeneric, LibraryModel } from "./parse-luals";
15
21
 
@@ -28,22 +34,22 @@ function round3(value: number): number {
28
34
  }
29
35
 
30
36
  /**
31
- * Build the fidelity report for one namespace. `knownNames` is drawn from the
32
- * model's own interface and alias names so a reference to a sibling library type
33
- * resolves rather than falling to `unknown`. Every field, every param and return
34
- * of every method and module function, and every alias expression is mapped;
35
- * `undocumentedMembers` counts fields/methods/moduleFunctions whose doc or brief
36
- * is empty. Deterministic; no I/O.
37
+ * Build the fidelity report for one namespace. Tokens map through the same
38
+ * `buildModelContext` the emitter and the api-doc lowering use, so all three agree
39
+ * on what resolves the model's own interface and alias names, plus the
40
+ * `externalTypes` tokens reached through a cross-module import and all three
41
+ * reject an external alias that collides with a declared name. Every field, every
42
+ * param and return of every method and module function, and every alias expression
43
+ * is mapped; `undocumentedMembers` counts fields/methods/moduleFunctions whose doc
44
+ * or brief is empty. Deterministic; no I/O.
37
45
  */
38
46
  export function buildFidelityReport(
39
47
  namespace: string,
40
48
  model: LibraryModel,
41
49
  typeRenames: Record<string, string>,
50
+ externalTypes?: Record<string, ExternalTypeRef>,
42
51
  ): FidelityReport {
43
- const knownNames = new Set<string>();
44
- for (const iface of model.interfaces) knownNames.add(iface.name);
45
- for (const alias of model.aliases) knownNames.add(alias.name);
46
- const ctx: MapContext = { knownNames, typeRenames };
52
+ const ctx = buildModelContext(model, typeRenames, externalTypes);
47
53
 
48
54
  let totalMembers = 0;
49
55
  let totalTypeTokens = 0;
@@ -80,12 +86,17 @@ export function buildFidelityReport(
80
86
  );
81
87
  }
82
88
  mapConstraints(iface.generics, ifaceCtx);
89
+ // A class `@overload fun(...)` is emitted as a call signature, so its type token
90
+ // counts toward coverage exactly like a field or method type.
91
+ for (const overload of iface.overloads ?? []) mapTokens([overload.type], ifaceCtx);
83
92
  for (const field of iface.fields) {
93
+ if (!isPublicField(field)) continue;
84
94
  totalMembers++;
85
95
  if (undocumented(field.doc)) undocumentedMembers++;
86
96
  mapTokens(field.types, ifaceCtx);
87
97
  }
88
98
  for (const method of iface.methods) {
99
+ if (!isPublicMethod(method)) continue;
89
100
  totalMembers++;
90
101
  if (undocumented(method.brief)) undocumentedMembers++;
91
102
  const methodCtx = scopeGenerics(ifaceCtx, method.generics);
@@ -96,6 +107,7 @@ export function buildFidelityReport(
96
107
  }
97
108
 
98
109
  for (const fn of model.moduleFunctions) {
110
+ if (!isPublicMethod(fn)) continue;
99
111
  totalMembers++;
100
112
  if (undocumented(fn.brief)) undocumentedMembers++;
101
113
  const fnCtx = scopeGenerics(ctx, fn.generics);
@@ -13,6 +13,7 @@
13
13
  * verbatim and the emitter sanitizes it later.
14
14
  */
15
15
 
16
+ import { luaMultiReturn } from "@defold-typescript/types";
16
17
  import { CORE_TYPE_RENAMES } from "./sync-library-types";
17
18
 
18
19
  export interface MapContext {
@@ -52,6 +53,24 @@ const SCALARS: Readonly<Record<string, string>> = {
52
53
  any: "unknown",
53
54
  };
54
55
 
56
+ /**
57
+ * LuaLS's bare `function` — any callable, signature unspecified. Params are `any[]`
58
+ * because under `strictFunctionTypes` an `unknown[]` rest would reject every concrete
59
+ * callback a consumer passes; the return stays `unknown` because return position is
60
+ * covariant, so `any` there would only leak unchecked values into call sites.
61
+ */
62
+ const CALLABLE_UNSPECIFIED = "(...args: any[]) => unknown";
63
+
64
+ /**
65
+ * LuaLS's two placeholders: the throwaway param name `_`, and a bare `...` in vararg
66
+ * or return position. Neither declares a type at all, so lowering it to `unknown`
67
+ * loses nothing an author wrote — the same reasoning that already exempts `any` from
68
+ * the fallback count. The boundary is deliberate: an untyped `self` or `ctx` *is* an
69
+ * upstream omission and stays recorded.
70
+ */
71
+ const LUALS_THROWAWAY_PARAM = "_";
72
+ export const LUALS_VARARG_TOKEN = "...";
73
+
55
74
  /**
56
75
  * Split `s` on every top-level occurrence of the single-character `sep`, honoring
57
76
  * bracket depth and double-quoted string literals so a separator nested inside
@@ -126,7 +145,17 @@ function needsArrayParens(tsExpr: string): boolean {
126
145
  );
127
146
  }
128
147
 
129
- function mapFunction(token: string, ctx: MapContext, unknowns: string[]): string {
148
+ /**
149
+ * The mapped `(params)` list and `ret` type of a `fun(...)` token, shared by the
150
+ * arrow-form `mapFunction` and the colon-return `mapLualsCallSignature`. The only
151
+ * difference between the two consumers is the separator (`=>` vs `:`), so both the
152
+ * param handling (typed/untyped/vararg) and the single/multi-return logic live here.
153
+ */
154
+ function functionParts(
155
+ token: string,
156
+ ctx: MapContext,
157
+ unknowns: string[],
158
+ ): { paramList: string; ret: string } {
130
159
  const open = token.indexOf("(");
131
160
  const close = matchBracket(token, open);
132
161
  const paramsStr = token.slice(open + 1, close).trim();
@@ -136,21 +165,18 @@ function mapFunction(token: string, ctx: MapContext, unknowns: string[]): string
136
165
  const paramList = params
137
166
  .map((raw) => raw.trim())
138
167
  .map((part) => {
139
- if (part.startsWith("...")) {
168
+ if (part.startsWith(LUALS_VARARG_TOKEN)) {
140
169
  const after = part.slice(3).trim();
141
- let element: string;
142
- if (after.startsWith(":")) {
143
- element = mapToken(after.slice(1).trim(), ctx, unknowns);
144
- } else {
145
- element = "unknown";
146
- unknowns.push("...");
147
- }
170
+ const element = after.startsWith(":")
171
+ ? mapToken(after.slice(1).trim(), ctx, unknowns)
172
+ : "unknown";
148
173
  return `...args: ${needsArrayParens(element) ? `(${element})[]` : `${element}[]`}`;
149
174
  }
150
175
  const colon = splitTopLevel(part, ":");
151
176
  if (colon.length < 2) {
152
- // Untyped param (`self`, `_`, `ctx`): a recorded gap, not a silent `any`.
153
- unknowns.push(part);
177
+ // Untyped param (`self`, `ctx`): a recorded gap, not a silent `any`. A bare `_`
178
+ // is LuaLS's deliberate throwaway and records nothing.
179
+ if (part !== LUALS_THROWAWAY_PARAM) unknowns.push(part);
154
180
  return `${part}: unknown`;
155
181
  }
156
182
  const name = colon[0]?.trim() ?? "";
@@ -167,10 +193,18 @@ function mapFunction(token: string, ctx: MapContext, unknowns: string[]): string
167
193
  if (retTokens.length === 1) {
168
194
  ret = mapToken(retTokens[0] as string, ctx, unknowns);
169
195
  } else if (retTokens.length > 1) {
170
- const inner = retTokens.map((r) => mapToken(r, ctx, unknowns)).join(", ");
171
- ret = `LuaMultiReturn<[${inner}]>`;
196
+ const restTail = retTokens.at(-1) === LUALS_VARARG_TOKEN;
197
+ ret = luaMultiReturn(
198
+ retTokens.map((r) => mapToken(r, ctx, unknowns)),
199
+ restTail,
200
+ );
172
201
  }
173
202
  }
203
+ return { paramList, ret };
204
+ }
205
+
206
+ function mapFunction(token: string, ctx: MapContext, unknowns: string[]): string {
207
+ const { paramList, ret } = functionParts(token, ctx, unknowns);
174
208
  return `(${paramList}) => ${ret}`;
175
209
  }
176
210
 
@@ -200,15 +234,11 @@ function mapToken(raw: string, ctx: MapContext, unknowns: string[]): string {
200
234
 
201
235
  if (token === "") return "unknown";
202
236
 
203
- // Optional suffix.
204
- if (token.length > 1 && token.endsWith("?")) {
205
- const base = mapToken(token.slice(0, -1), ctx, unknowns);
206
- const members = splitTopLevel(base, "|").map((m) => m.trim());
207
- return members.includes("undefined") ? base : `${base} | undefined`;
208
- }
209
-
210
- // A `fun(...)` whose return follows the `)` keeps its return-type `|` inside the
211
- // function; splitting the union first would cut `fun(): a|b` into `(fun) | b`.
237
+ // A `fun(...)` whose return follows the `)` keeps its return-type `|` and `?` inside
238
+ // the function; splitting the union first would cut `fun(): a|b` into `(fun) | b`, and
239
+ // peeling the optional suffix first would turn `fun(): number?` — a function with an
240
+ // optional *return* into an optional function. The whole-function optional is spelled
241
+ // with explicit parentheses, `(fun(): number)?`, which does not match here.
212
242
  // `fun()|nil` (a `|` right after the `)`) falls through to the union split.
213
243
  if (/^fun\s*\(/.test(token)) {
214
244
  const close = matchBracket(token, token.indexOf("("));
@@ -218,6 +248,13 @@ function mapToken(raw: string, ctx: MapContext, unknowns: string[]): string {
218
248
  }
219
249
  }
220
250
 
251
+ // Optional suffix.
252
+ if (token.length > 1 && token.endsWith("?")) {
253
+ const base = mapToken(token.slice(0, -1), ctx, unknowns);
254
+ const members = splitTopLevel(base, "|").map((m) => m.trim());
255
+ return members.includes("undefined") ? base : `${wrapForUnion(base)} | undefined`;
256
+ }
257
+
221
258
  // Top-level union.
222
259
  const unionParts = splitTopLevel(token, "|");
223
260
  if (unionParts.length > 1) {
@@ -248,6 +285,15 @@ function mapToken(raw: string, ctx: MapContext, unknowns: string[]): string {
248
285
  // String literal — passthrough.
249
286
  if (token.startsWith('"') && token.endsWith('"')) return token;
250
287
 
288
+ // Signature-less callable. Placed after every composite branch so `function[]`,
289
+ // `function|nil`, and `fun(cb: function)` route through those first.
290
+ if (token === "function") return CALLABLE_UNSPECIFIED;
291
+
292
+ // Placeholder vararg in return position — not a `SCALARS` entry, which maps real Lua
293
+ // type names. Sits beside the callable branch so `...[]` and `...|nil` reach their
294
+ // structural handlers first.
295
+ if (token === LUALS_VARARG_TOKEN) return "unknown";
296
+
251
297
  // Scalars.
252
298
  const scalar = SCALARS[token];
253
299
  if (scalar !== undefined) return scalar;
@@ -275,6 +321,24 @@ export function mapLualsType(token: string, ctx: MapContext): MapResult {
275
321
  return { ts, unknowns };
276
322
  }
277
323
 
324
+ /**
325
+ * Map a `fun(...)` token (a class `@overload`) to a TypeScript **call signature** —
326
+ * the colon-return form `(params): ret` an interface uses to become callable, not
327
+ * the `=>` arrow a field/param function type takes. Shares the exact param/return
328
+ * computation as `mapFunction`, so nested callback params and multi-returns map
329
+ * identically. Throws on a non-`fun` token; the parser only ever records `fun(...)`
330
+ * overloads, so this guards a programming error rather than user input.
331
+ */
332
+ export function mapLualsCallSignature(token: string, ctx: MapContext): MapResult {
333
+ const trimmed = token.trim();
334
+ if (!/^fun\s*\(/.test(trimmed)) {
335
+ throw new Error(`mapLualsCallSignature: expected a "fun(...)" token, got "${token}".`);
336
+ }
337
+ const unknowns: string[] = [];
338
+ const { paramList, ret } = functionParts(trimmed, ctx, unknowns);
339
+ return { ts: `(${paramList}): ${ret}`, unknowns };
340
+ }
341
+
278
342
  /**
279
343
  * When `types` is exactly one `fun(self: <selfTypeName>, ...)` token — optionally
280
344
  * unioned with `nil` — whose first parameter is `self` typed as the enclosing
@@ -0,0 +1,187 @@
1
+ /**
2
+ * A line-oriented reader for the *public surface* of a plain-Lua module — the
3
+ * member names and parameter names a `local M = {} … return M` file declares.
4
+ *
5
+ * `sync-authored-types.ts` gates the authored/forked lane on a forked-vs-generated
6
+ * identity diff and records that there is no coverage comparison against a primary
7
+ * source. That holds for *types*: upstream Lua declares none. It does not hold for
8
+ * *surface*: names and arity are right there in the source, so a fork that drops a
9
+ * member or a trailing parameter is measurable. This module reads that side; the
10
+ * comparison lives in `authored-parity.ts`.
11
+ *
12
+ * The corpus is a handful of pinned files, so line scanning is what it needs — a
13
+ * Lua grammar would buy nothing here. Two rules keep it honest:
14
+ *
15
+ * - **Column 0 only.** A definition is a member when it starts the line. This is
16
+ * what keeps `nakama/util/log.lua`'s `M.log = noop` — reassigned inside three
17
+ * different function bodies — out of the surface, and it is why the reader never
18
+ * needs to know where a block ends.
19
+ * - **Loud failure over silent undercount.** A missing `return <name>` or a
20
+ * parameter list that does not close on its own line throws, as does a
21
+ * `return setmetatable(M, …)` whose metatable carries `__index` or is named by a
22
+ * variable whose keys this reader cannot see — either could add members no column-0
23
+ * scan will find. The whole point of the instrument is that a dropped member is
24
+ * visible; a parser that quietly skips what it cannot read would inflate every
25
+ * coverage number it feeds.
26
+ */
27
+
28
+ export interface LuaMember {
29
+ name: string;
30
+ /** Absent for a non-callable field (`M.SOME_CONSTANT = "X"`), so a field can
31
+ * never be confused with a zero-arity function. */
32
+ params?: string[];
33
+ /** True when the definition ends in `...`, which is not a named parameter. */
34
+ varargs: boolean;
35
+ /** The `---` LuaDoc block immediately above the definition, comment markers
36
+ * stripped, one interior blank line crossed; empty when the block is absent or
37
+ * opens with a plain `--`. */
38
+ doc: string;
39
+ /** True when a comment block *was* there and the `---` rule declined it, which `doc`
40
+ * alone cannot say — an empty `doc` otherwise means upstream documented nothing. */
41
+ refusedDoc: boolean;
42
+ }
43
+
44
+ export interface LuaSurface {
45
+ moduleLocal: string;
46
+ members: LuaMember[];
47
+ }
48
+
49
+ const RETURN_LINE = /^return\s+([A-Za-z_][A-Za-z0-9_]*)\s*;?\s*$/;
50
+ const RETURN_SETMETATABLE = /^return\s+setmetatable\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,/;
51
+ const IDENTIFIER = "[A-Za-z_][A-Za-z0-9_]*";
52
+
53
+ /** The metatable argument of a `return setmetatable(<name>, …)`, refused unless its
54
+ * keys are readable and none of them delegates. `text` runs from the comma to the end
55
+ * of the source rather than to the matching brace: over-reading can only refuse a file
56
+ * loudly, where under-reading would accept a delegating module and report its short
57
+ * surface as complete. */
58
+ function assertNoDelegation(text: string): void {
59
+ if (text.trimStart().startsWith("{") === false) {
60
+ throw new Error(
61
+ "lua surface: the returned `setmetatable` is given a metatable this reader cannot see the keys of, so a delegated member would be invisible — inline the table literal or measure the module another way.",
62
+ );
63
+ }
64
+ if (text.includes("__index")) {
65
+ throw new Error(
66
+ "lua surface: the returned metatable carries `__index`, so members reached through it would be invisible to a column-0 scan and the surface would read as complete while short.",
67
+ );
68
+ }
69
+ }
70
+
71
+ function resolveModuleLocal(lines: string[]): string {
72
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
73
+ const line = lines[index] as string;
74
+ const bare = RETURN_LINE.exec(line);
75
+ if (bare) return bare[1] as string;
76
+ const wrapped = RETURN_SETMETATABLE.exec(line);
77
+ if (wrapped) {
78
+ assertNoDelegation(
79
+ [line.slice((wrapped[0] as string).length), ...lines.slice(index + 1)].join("\n"),
80
+ );
81
+ return wrapped[1] as string;
82
+ }
83
+ }
84
+ throw new Error(
85
+ "lua surface: the source has no trailing `return <name>`, so the module-local name cannot be derived.",
86
+ );
87
+ }
88
+
89
+ /** The parameter names between the parentheses opened at `open` on `line`, with a
90
+ * `...` tail reported separately. Throws when the list does not close on the same
91
+ * line rather than dropping the member. */
92
+ function readParams(
93
+ line: string,
94
+ open: number,
95
+ lineNumber: number,
96
+ ): { params: string[]; varargs: boolean } {
97
+ const close = line.indexOf(")", open);
98
+ if (close === -1) {
99
+ throw new Error(
100
+ `lua surface: line ${lineNumber} opens a parameter list that does not close on the same line — ${line.trim()}`,
101
+ );
102
+ }
103
+ const raw = line
104
+ .slice(open + 1, close)
105
+ .split(",")
106
+ .map((part) => part.trim())
107
+ .filter((part) => part !== "");
108
+ return { params: raw.filter((part) => part !== "..."), varargs: raw.includes("...") };
109
+ }
110
+
111
+ /** The contiguous run of comment lines ending at `index - 1`, in source order. */
112
+ function readSegment(lines: string[], index: number): string[] {
113
+ const segment: string[] = [];
114
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
115
+ const line = (lines[cursor] as string).trim();
116
+ if (!line.startsWith("--") || line.startsWith("--[[")) break;
117
+ segment.unshift(line);
118
+ }
119
+ return segment;
120
+ }
121
+
122
+ /** The contiguous comment block ending at `index - 1`, kept only when it opens
123
+ * with `---`. LuaDoc in this corpus is a `---` summary followed by plain `--`
124
+ * `@param`/`@return` lines, so the marker on the *first* line is what decides.
125
+ *
126
+ * A single blank line inside the run is crossed, and only when the segment
127
+ * attached to the definition opens no block of its own — upstream wraps a long
128
+ * `@param` list around a blank, which would otherwise truncate the block to its
129
+ * tag half and lose the summary above. A segment carrying its own `---` is never
130
+ * absorbed into the one below it, so a blank-separated section header stays out. */
131
+ function readDoc(lines: string[], index: number): Pick<LuaMember, "doc" | "refusedDoc"> {
132
+ const attached = readSegment(lines, index);
133
+ if (attached.length === 0) return { doc: "", refusedDoc: false };
134
+ const above = index - attached.length - 1;
135
+ const block =
136
+ attached.some((line) => line.startsWith("---")) ||
137
+ above < 0 ||
138
+ (lines[above] as string).trim() !== ""
139
+ ? attached
140
+ : [...readSegment(lines, above), ...attached];
141
+ if (!(block[0] as string).startsWith("---")) return { doc: "", refusedDoc: true };
142
+ const doc = block
143
+ .map((line) => line.replace(/^-+\s?/, "").trimEnd())
144
+ .join("\n")
145
+ .trim();
146
+ return { doc, refusedDoc: false };
147
+ }
148
+
149
+ /**
150
+ * The public surface of a Lua module source: the module-local name it returns and
151
+ * every member assigned to it at column 0, in source order. A name defined more
152
+ * than once keeps its first position and its last definition, matching Lua.
153
+ */
154
+ export function parseLuaSurface(source: string): LuaSurface {
155
+ const lines = source.split("\n");
156
+ const moduleLocal = resolveModuleLocal(lines);
157
+ const definition = new RegExp(`^function\\s+${moduleLocal}\\.(${IDENTIFIER})\\s*\\(`);
158
+ const assignment = new RegExp(`^${moduleLocal}\\.(${IDENTIFIER})\\s*=\\s*(.*)$`);
159
+ const assignedFunction = /^function\s*\(/;
160
+
161
+ const members = new Map<string, LuaMember>();
162
+ const record = (member: LuaMember): void => {
163
+ members.set(member.name, member);
164
+ };
165
+
166
+ for (const [index, line] of lines.entries()) {
167
+ const defined = definition.exec(line);
168
+ if (defined) {
169
+ const { params, varargs } = readParams(line, defined[0].length - 1, index + 1);
170
+ record({ name: defined[1] as string, params, varargs, ...readDoc(lines, index) });
171
+ continue;
172
+ }
173
+ const assigned = assignment.exec(line);
174
+ if (!assigned) continue;
175
+ const name = assigned[1] as string;
176
+ const rhs = assigned[2] as string;
177
+ if (!assignedFunction.test(rhs)) {
178
+ record({ name, varargs: false, ...readDoc(lines, index) });
179
+ continue;
180
+ }
181
+ const open = line.indexOf("(", line.length - rhs.length);
182
+ const { params, varargs } = readParams(line, open, index + 1);
183
+ record({ name, params, varargs, ...readDoc(lines, index) });
184
+ }
185
+
186
+ return { moduleLocal, members: [...members.values()] };
187
+ }