@defold-typescript/types 0.35.0 → 0.37.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 (36) hide show
  1. package/api-signatures.json +9406 -18
  2. package/examples/translations.json +11 -11
  3. package/generated/b2d.d.ts +1 -1
  4. package/generated/b2d_body.d.ts +6 -4
  5. package/generated/b2d_fixture.d.ts +6 -4
  6. package/generated/b2d_shape.d.ts +6 -4
  7. package/generated/factory.d.ts +11 -10
  8. package/generated/render.d.ts +5 -5
  9. package/generated/resource.d.ts +4 -4
  10. package/generated/sys.d.ts +2 -2
  11. package/generated/versions/defold-1.12.4/b2d.d.ts +1 -1
  12. package/generated/versions/defold-1.12.4/factory.d.ts +11 -10
  13. package/generated/versions/defold-1.12.4/render.d.ts +5 -5
  14. package/generated/versions/defold-1.12.4/resource.d.ts +4 -4
  15. package/generated/versions/defold-1.12.4/sys.d.ts +2 -2
  16. package/generated/versions/defold-1.12.4/window.d.ts +1 -1
  17. package/generated/versions/defold-1.12.4/zlib.d.ts +2 -1
  18. package/generated/versions/defold-1.13.0/b2d.d.ts +1 -1
  19. package/generated/versions/defold-1.13.0/b2d_body.d.ts +6 -4
  20. package/generated/versions/defold-1.13.0/b2d_fixture.d.ts +6 -4
  21. package/generated/versions/defold-1.13.0/b2d_shape.d.ts +6 -4
  22. package/generated/versions/defold-1.13.0/factory.d.ts +11 -10
  23. package/generated/versions/defold-1.13.0/render.d.ts +5 -5
  24. package/generated/versions/defold-1.13.0/resource.d.ts +4 -4
  25. package/generated/versions/defold-1.13.0/sys.d.ts +2 -2
  26. package/generated/versions/defold-1.13.0/window.d.ts +1 -1
  27. package/generated/versions/defold-1.13.0/zlib.d.ts +2 -1
  28. package/generated/window.d.ts +1 -1
  29. package/generated/zlib.d.ts +2 -1
  30. package/index.d.ts +1 -0
  31. package/package.json +5 -2
  32. package/scripts/generate-api-signatures.ts +33 -8
  33. package/src/emit-dts.ts +355 -71
  34. package/src/index.ts +1 -0
  35. package/src/render-overloads.d.ts +17 -0
  36. package/src/return-correction-provenance.ts +306 -0
package/src/emit-dts.ts CHANGED
@@ -222,7 +222,9 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
222
222
  // is the TSTL non-string-key Lua-table idiom and resolves the same way the
223
223
  // already-emitted `LuaMultiReturn` does. The audit consults this map too, so the
224
224
  // gate and the emitted surface stay coupled; a slot not curated here still
225
- // surfaces under `recordTables` until a human adds it.
225
+ // surfaces under `recordTables` until a human adds it. Key and value are both
226
+ // `A | B` union tokens split per token, so a key whose doc names constants
227
+ // brands each of them rather than widening to `number`.
226
228
  export const MAPPING_TABLE_SLOTS: ReadonlyMap<string, { key: string; value: string }> = new Map([
227
229
  ["gui.clone_tree", { key: "hash", value: "node" }],
228
230
  ["gui.get_tree", { key: "hash", value: "node" }],
@@ -243,6 +245,32 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
243
245
  ["editor.tx.add", 'Opaque<"transaction_step">'],
244
246
  ]);
245
247
 
248
+ // A function whose `returnvalues` declare a concrete type while the same slot's
249
+ // prose documents a `nil` alternative. Distinct from RETURN_TYPE_OVERRIDES
250
+ // above, which fills an *empty* `returnvalues`: these contradict a token
251
+ // upstream explicitly declares, so each entry records it and
252
+ // `return-correction-provenance.test.ts` pins it against every vendored
253
+ // ref-doc. Upstream adding `nil` to `types` hands the case to the top-level
254
+ // `nil` projection in `mapSlotUnion` and reds the entry, which is then deleted
255
+ // rather than re-pinned.
256
+ export interface ReturnTypeCorrection {
257
+ readonly ts: string;
258
+ readonly upstream: readonly string[];
259
+ readonly reason: string;
260
+ }
261
+
262
+ // Keyed by FQN, mirroring PROPERTY_TYPE_CORRECTIONS.
263
+ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection> = new Map([
264
+ [
265
+ "b2d.get_body",
266
+ {
267
+ ts: 'Opaque<"b2Body"> | undefined',
268
+ upstream: ["b2Body"],
269
+ reason: "the body if successful. Otherwise nil.",
270
+ },
271
+ ],
272
+ ]);
273
+
246
274
  // A property whose upstream `<span class="type">` states a type the engine does
247
275
  // not use. Unlike every other override here, which fills a gap upstream left
248
276
  // empty, these contradict a token upstream explicitly declares — so each entry
@@ -408,6 +436,18 @@ export function documentedConstantTokens(text: string, universe: ReadonlySet<str
408
436
  return [...out];
409
437
  }
410
438
 
439
+ // The constants a `table` slot's doc names as the table's *keys*, as opposed to
440
+ // the far commoner shape where a field's *value* is a constant
441
+ // (`physics.get_shape`'s `type`, `resource.create_texture`'s `format`). Upstream
442
+ // marks the difference in prose — render.clear reads "table with keys specifying
443
+ // which buffers to clear ... Available keys are:" — so a key mention alongside a
444
+ // named constant is the discriminator. Empty means the slot has no documented
445
+ // constant keys, which is true of every `table` slot but one today.
446
+ export function documentedConstantKeyTokens(doc: string, universe: ReadonlySet<string>): string[] {
447
+ if (!/\bkeys?\b/i.test(doc.replace(/<[^>]*>/g, ""))) return [];
448
+ return documentedConstantTokens(doc, universe);
449
+ }
450
+
411
451
  type SlotDocLookup = (slotKey: string) => string | undefined;
412
452
 
413
453
  function constantSlotTokens(
@@ -805,6 +845,38 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
805
845
  ],
806
846
  },
807
847
  ],
848
+ // resource.get_render_target_info's return `<dl>` writes one key as
849
+ // `<dt>'attachments'</dt>` — quoted rather than wrapped in `<code>` — so
850
+ // TABLE_FIELD drops it and parseTableFields hoists the seven per-attachment
851
+ // fields to the top level, contradicting the `info.attachments[0].handle`
852
+ // access in the doc's own `@example`. Same shape and same remedy as
853
+ // font.get_info:return:info above: replace the mis-parsed field list with the
854
+ // faithful nested isList object. `texture` is documented "only available if
855
+ // the render target passed in is a resource", hence optional.
856
+ [
857
+ "resource.get_render_target_info:return:table",
858
+ {
859
+ kind: "object",
860
+ fields: [
861
+ { name: "handle", types: ["number"] },
862
+ {
863
+ name: "attachments",
864
+ types: ["table"],
865
+ isList: true,
866
+ fields: [
867
+ { name: "handle", types: ["number"] },
868
+ { name: "width", types: ["number"] },
869
+ { name: "height", types: ["number"] },
870
+ { name: "depth", types: ["number"] },
871
+ { name: "mipmaps", types: ["number"] },
872
+ { name: "type", types: ["number"] },
873
+ { name: "buffer_type", types: ["number"] },
874
+ { name: "texture", types: ["hash"], optional: true },
875
+ ],
876
+ },
877
+ ],
878
+ },
879
+ ],
808
880
  // profiler.view_recorded_frame's `frame_index` is a typed `<ul>` "specify one
809
881
  // of the following": `distance` and `frame`, both numeric frame offsets. The
810
882
  // <li> items carry no `<span class="type">`, so parseTableFields recovers
@@ -830,11 +902,20 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
830
902
  // render.predicate's `tags` is "table of tags ... can be of either hash or
831
903
  // string type", a homogeneous array whose element is the string|hash union.
832
904
  ["render.predicate:param:tags", { kind: "array", element: ["string", "hash"] }],
833
- // render.clear's `buffers` maps the numeric graphics.BUFFER_* constants to
834
- // their clear value: vector4 for the color buffer, number for depth/stencil.
835
- // The mapping value is the `number | vector4` union; the mapping string branch
836
- // splits the union token, so the value emits `number | Vector4`.
837
- ["render.clear:param:buffers", { kind: "mapping", key: "number", value: "number | vector4" }],
905
+ // render.clear's `buffers` maps the graphics.BUFFER_TYPE_* constants its doc
906
+ // names as keys to their clear value: vector4 for the color buffer, number for
907
+ // depth/stencil. Both sides are union tokens the mapping string branch splits,
908
+ // so the key emits the three branded constants and the value `number | Vector4`.
909
+ // The key is branded rather than `number`, which is the narrowing users migrate
910
+ // to via the authored `render.ClearBufferKey` alias.
911
+ [
912
+ "render.clear:param:buffers",
913
+ {
914
+ kind: "mapping",
915
+ key: "graphics.BUFFER_TYPE_COLOR0_BIT | graphics.BUFFER_TYPE_DEPTH_BIT | graphics.BUFFER_TYPE_STENCIL_BIT",
916
+ value: "number | vector4",
917
+ },
918
+ ],
838
919
  // go.on_input and gui.on_input share the same well-known InputAction shape
839
920
  // (the on_input function description lists the well-known fields: value,
840
921
  // pressed, released, repeated, x/y/screen_x/screen_y/dx/dy/screen_dx/screen_dy,
@@ -1346,6 +1427,95 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
1346
1427
  ["material.get_vertex_attributes:return:table:value", ATTRIBUTE_VALUE_TS],
1347
1428
  ]);
1348
1429
 
1430
+ // A return-slot field upstream documents as platform-gated or nil-valued while
1431
+ // declaring it like any other. Lua removes a key assigned `nil`, so "might be
1432
+ // nil if not available" and "only available on iOS and Android" describe the
1433
+ // same runtime state — the key is absent — and the correction is always `?`,
1434
+ // never a required field widened to `| undefined`.
1435
+ //
1436
+ // Separate from TABLE_FIELD_TYPE_OVERRIDES because presence is the other axis:
1437
+ // the parser reads these fields' types correctly and stays authoritative for
1438
+ // them, so an entry here restates no type and cannot drift when upstream
1439
+ // retypes a field. `upstream` is pinned rather than consumed, and `evidence` is
1440
+ // the decoded prose `return-correction-provenance.test.ts` searches the slot doc
1441
+ // for; both red the entry once upstream marks the field or drops the gate.
1442
+ //
1443
+ // Keyed `<element>:<kind>:<slot>:<field>`, the TABLE_FIELD_TYPE_OVERRIDES shape.
1444
+ export interface ReturnFieldOptionalityCorrection {
1445
+ readonly upstream: readonly string[];
1446
+ readonly evidence: string;
1447
+ }
1448
+
1449
+ // The platform-gate markers upstream renders as `<span class="icon-*">` decode
1450
+ // to `[ios]`, `[android]`, `[html5]` — `user_agent` states its gate through the
1451
+ // marker alone, with no availability sentence to pin.
1452
+ export const RETURN_FIELD_OPTIONALITY_CORRECTIONS: ReadonlyMap<
1453
+ string,
1454
+ ReturnFieldOptionalityCorrection
1455
+ > = new Map([
1456
+ [
1457
+ "sys.get_sys_info:return:sys_info:device_model",
1458
+ { upstream: ["string"], evidence: "[ios][android] Only available on iOS and Android." },
1459
+ ],
1460
+ [
1461
+ "sys.get_sys_info:return:sys_info:manufacturer",
1462
+ { upstream: ["string"], evidence: "[ios][android] Only available on iOS and Android." },
1463
+ ],
1464
+ [
1465
+ "sys.get_sys_info:return:sys_info:device_ident",
1466
+ {
1467
+ upstream: ["string"],
1468
+ evidence: '[ios] "identifierForVendor" on iOS. [android] "android_id" on Android.',
1469
+ },
1470
+ ],
1471
+ [
1472
+ "sys.get_sys_info:return:sys_info:user_agent",
1473
+ { upstream: ["string"], evidence: "[html5] The HTTP user agent" },
1474
+ ],
1475
+ [
1476
+ "sys.get_ifaddrs:return:ifaddrs:address",
1477
+ { upstream: ["string"], evidence: "might be nil if not available." },
1478
+ ],
1479
+ [
1480
+ "sys.get_ifaddrs:return:ifaddrs:mac",
1481
+ { upstream: ["string"], evidence: "might be nil if not available." },
1482
+ ],
1483
+ ]);
1484
+
1485
+ // Mark a parser-recovered field optional from RETURN_FIELD_OPTIONALITY_CORRECTIONS,
1486
+ // leaving every other field — and every field's type — untouched. Sets the
1487
+ // `optional` flag `TableField` already carries and `inlineTableType` already
1488
+ // reads, so no `mapType` is needed and no correction can change a type.
1489
+ //
1490
+ // A correction keyed to this slot that matches no recovered field throws rather
1491
+ // than no-opping: a field upstream renamed would otherwise leave a stale entry
1492
+ // looking applied while the emitted declaration silently re-widened.
1493
+ // Returns a new array; never mutates the parser's result.
1494
+ export function applyFieldOptionalityCorrections(
1495
+ elementName: string,
1496
+ slotKind: "param" | "return" | undefined,
1497
+ slotName: string | undefined,
1498
+ fields: readonly TableField[],
1499
+ ): TableField[] {
1500
+ if (slotKind === undefined || slotName === undefined) return [...fields];
1501
+ const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
1502
+ const corrected = fields.map((field) =>
1503
+ RETURN_FIELD_OPTIONALITY_CORRECTIONS.has(`${prefix}${field.name}`)
1504
+ ? { ...field, optional: true }
1505
+ : field,
1506
+ );
1507
+ const recovered = new Set(fields.map((field) => field.name));
1508
+ const stale = [...RETURN_FIELD_OPTIONALITY_CORRECTIONS.keys()].filter(
1509
+ (key) => key.startsWith(prefix) && !recovered.has(key.slice(prefix.length)),
1510
+ );
1511
+ if (stale.length > 0) {
1512
+ throw new Error(
1513
+ `field-optionality correction names no recovered field: ${stale.sort().join(", ")}`,
1514
+ );
1515
+ }
1516
+ return corrected;
1517
+ }
1518
+
1349
1519
  // Pin a parser-recovered field's TS type from TABLE_FIELD_TYPE_OVERRIDES,
1350
1520
  // leaving every other field parser-authoritative. A field the override does not
1351
1521
  // name is returned unchanged; a field it names gains a `tsType` (emitted
@@ -1582,7 +1752,11 @@ export function buildTableDocResolver(sources: readonly TableDocSource[]): Table
1582
1752
  return (elementName) => docByName.get(elementName);
1583
1753
  }
1584
1754
 
1585
- export function parseTableFields(doc: string, resolver?: TableDocResolver): TableField[] | null {
1755
+ export function parseTableFields(
1756
+ doc: string,
1757
+ resolver?: TableDocResolver,
1758
+ slotName?: string,
1759
+ ): TableField[] | null {
1586
1760
  const fields: TableField[] = [];
1587
1761
  for (const match of doc.matchAll(TABLE_FIELD)) {
1588
1762
  const name = (match[1] ?? "").trim();
@@ -1601,14 +1775,20 @@ export function parseTableFields(doc: string, resolver?: TableDocResolver): Tabl
1601
1775
  }
1602
1776
  // Nested recovery, scoped to the one unambiguous shape: a `<dl>` declaring a
1603
1777
  // single `table`-typed field whose keys sit in an immediately-following
1604
- // top-level `<ul>` typed-field list (`window.get_safe_area`). Attach the
1605
- // `<ul>` fields as that field's nested shape so the slot recovers as a nested
1606
- // object instead of a `Record`.
1778
+ // top-level `<ul>` typed-field list. Attach the `<ul>` fields as that field's
1779
+ // nested shape so the slot recovers as a nested object instead of a `Record`.
1780
+ //
1781
+ // Unless the header restates the slot it describes (`window.get_safe_area`'s
1782
+ // `safe_area` return documented under a `safe_area` header): that is a
1783
+ // restatement, not a nesting level, and wrapping it emits a level the engine
1784
+ // never returns. Only a caller that knows the slot name can tell the two
1785
+ // apart, so the `<ul>` keys are returned directly when they match.
1607
1786
  if (fields.length === 1) {
1608
1787
  const only = fields[0];
1609
1788
  if (only && only.types.length === 1 && only.types[0] === "table") {
1610
1789
  const nested = parseUlFields(doc);
1611
1790
  if (nested.length > 0) {
1791
+ if (slotName !== undefined && slotName === only.name) return nested;
1612
1792
  only.fields = nested;
1613
1793
  return [only];
1614
1794
  }
@@ -1883,10 +2063,25 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1883
2063
  return `${lines.join("\n")}\n`;
1884
2064
  }
1885
2065
 
2066
+ /**
2067
+ * Every documented slot's rendered TS type, keyed `param|return:<position>:<raw
2068
+ * ref-doc name>`. The position is part of the key because a multi-return can
2069
+ * carry several unnamed slots (`push.schedule` renders two), which a name-only
2070
+ * key would collapse into one. Each value appears verbatim inside the symbol's
2071
+ * own `tsSignature`, so a consumer rendering a slot's type beside that signature
2072
+ * cannot contradict it.
2073
+ */
2074
+ export type SlotTypes = Readonly<Record<string, string>>;
2075
+
2076
+ function slotKey(kind: "param" | "return", position: number, name: string): string {
2077
+ return `${kind}:${position}:${name}`;
2078
+ }
2079
+
1886
2080
  /** One emitted symbol's identity paired with its rendered TS signature text. */
1887
2081
  export interface SymbolSignature {
1888
2082
  readonly identity: ApiSymbolIdentity;
1889
2083
  readonly tsSignature: string;
2084
+ readonly slotTypes: SlotTypes;
1890
2085
  }
1891
2086
 
1892
2087
  /**
@@ -1931,9 +2126,14 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1931
2126
  });
1932
2127
  const emitName = (name: string): string => (TS_RESERVED_NAMES.has(name) ? `_${name}` : name);
1933
2128
 
2129
+ // A member with no call list (constant, variable, property, typedef) documents
2130
+ // no slot, so it reports an empty map rather than being absent from the ledger.
2131
+ const NO_SLOTS: SlotTypes = {};
2132
+
1934
2133
  for (const fn of module.functions) {
1935
2134
  const prepared = prepareFunction(fn, prefix);
1936
2135
  if (prepared === null) continue;
2136
+ const slotTypes: Record<string, string> = {};
1937
2137
  out.push({
1938
2138
  identity: fnIdentity(fn),
1939
2139
  tsSignature: emitFunction(
@@ -1943,7 +2143,9 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1943
2143
  resolver,
1944
2144
  constantTokens,
1945
2145
  urlParameters,
2146
+ slotTypes,
1946
2147
  ),
2148
+ slotTypes,
1947
2149
  });
1948
2150
  }
1949
2151
 
@@ -1957,9 +2159,11 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1957
2159
  signature: "",
1958
2160
  },
1959
2161
  tsSignature: emitVariable(v, emitName(v.name), mapType),
2162
+ slotTypes: NO_SLOTS,
1960
2163
  });
1961
2164
  }
1962
2165
  for (const fn of nested.functions) {
2166
+ const slotTypes: Record<string, string> = {};
1963
2167
  out.push({
1964
2168
  identity: fnIdentity(fn.original),
1965
2169
  tsSignature: emitFunction(
@@ -1969,16 +2173,27 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1969
2173
  resolver,
1970
2174
  constantTokens,
1971
2175
  urlParameters,
2176
+ slotTypes,
1972
2177
  ),
2178
+ slotTypes,
1973
2179
  });
1974
2180
  }
1975
2181
 
1976
2182
  const handleGroups = collectHandleMethodGroups(module);
1977
2183
  for (const group of handleGroups.values()) {
1978
2184
  for (const prepared of group) {
2185
+ const slotTypes: Record<string, string> = {};
1979
2186
  out.push({
1980
2187
  identity: fnIdentity(prepared.original),
1981
- tsSignature: emitMethod(prepared, mapType, resolver, constantTokens, urlParameters),
2188
+ tsSignature: emitMethod(
2189
+ prepared,
2190
+ mapType,
2191
+ resolver,
2192
+ constantTokens,
2193
+ urlParameters,
2194
+ slotTypes,
2195
+ ),
2196
+ slotTypes,
1982
2197
  });
1983
2198
  }
1984
2199
  }
@@ -1989,6 +2204,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1989
2204
  out.push({
1990
2205
  identity: { namespace: module.namespace, kind: "CONSTANT", name: c.name, signature: "" },
1991
2206
  tsSignature: `const ${prepared.name}: ${brandType(prepared.fqn)};`,
2207
+ slotTypes: NO_SLOTS,
1992
2208
  });
1993
2209
  }
1994
2210
 
@@ -1998,6 +2214,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1998
2214
  out.push({
1999
2215
  identity: { namespace: module.namespace, kind: "VARIABLE", name: v.name, signature: "" },
2000
2216
  tsSignature: emitVariable(prepared, emitName(prepared.name), mapType),
2217
+ slotTypes: NO_SLOTS,
2001
2218
  });
2002
2219
  }
2003
2220
 
@@ -2005,6 +2222,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2005
2222
  out.push({
2006
2223
  identity: { namespace: module.namespace, kind: "PROPERTY", name: p.name, signature: "" },
2007
2224
  tsSignature: emitPropertyMember(p, mapType, module.namespace),
2225
+ slotTypes: NO_SLOTS,
2008
2226
  });
2009
2227
  }
2010
2228
 
@@ -2013,6 +2231,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2013
2231
  out.push({
2014
2232
  identity: { namespace: module.namespace, kind: "TYPEDEF", name: t.name, signature: "" },
2015
2233
  tsSignature: `type ${t.name} = Opaque<"${t.name}">;`,
2234
+ slotTypes: NO_SLOTS,
2016
2235
  });
2017
2236
  }
2018
2237
 
@@ -2208,6 +2427,7 @@ function memberSignature(
2208
2427
  resolver: TableDocResolver,
2209
2428
  constantTokens: ConstantSlotTokens,
2210
2429
  urlParameters: UrlParameterTable,
2430
+ slotSink?: Record<string, string>,
2211
2431
  ): string {
2212
2432
  const original = prepared.original.parameters;
2213
2433
  const elementName = prepared.original.name;
@@ -2216,22 +2436,19 @@ function memberSignature(
2216
2436
  const positional = (varargIndex === -1 ? original : original.slice(0, varargIndex)).map((p, i) =>
2217
2437
  emitParameter(p, i, i >= cutoff, mapType, resolver, constantTokens, elementName, urlParameters),
2218
2438
  );
2219
- const params = (
2439
+ const rest =
2220
2440
  varargIndex === -1
2221
- ? positional
2222
- : [
2223
- ...positional,
2224
- emitRestParameter(
2225
- original,
2226
- varargIndex,
2227
- mapType,
2228
- resolver,
2229
- constantTokens,
2230
- elementName,
2231
- urlParameters,
2232
- ),
2233
- ]
2234
- ).join(", ");
2441
+ ? null
2442
+ : emitRestParameter(
2443
+ original,
2444
+ varargIndex,
2445
+ mapType,
2446
+ resolver,
2447
+ constantTokens,
2448
+ elementName,
2449
+ urlParameters,
2450
+ );
2451
+ const params = [...positional.map((p) => p.text), ...(rest ? [rest.text] : [])].join(", ");
2235
2452
  const ret = emitReturn(
2236
2453
  prepared.original.returnValues,
2237
2454
  mapType,
@@ -2241,12 +2458,28 @@ function memberSignature(
2241
2458
  );
2242
2459
  const predicateToken = TYPE_PREDICATES.get(elementName);
2243
2460
  const soleParam = original[0];
2244
- if (
2461
+ const isPredicate =
2245
2462
  predicateToken !== undefined &&
2246
2463
  soleParam !== undefined &&
2247
2464
  original.length === 1 &&
2248
- ret.type === "boolean"
2249
- ) {
2465
+ ret.type === "boolean";
2466
+ if (slotSink !== undefined) {
2467
+ for (const [i, p] of positional.entries()) {
2468
+ const raw = original[i];
2469
+ if (raw) slotSink[slotKey("param", i, raw.name)] = p.ts;
2470
+ }
2471
+ for (const slot of rest?.slots ?? []) {
2472
+ slotSink[slotKey("param", slot.position, slot.name)] = slot.ts;
2473
+ }
2474
+ // A predicate renders `arg is T` in place of the mapped `boolean`, so its
2475
+ // return slot has no text in the signature to stand on and is not reported.
2476
+ if (!isPredicate) {
2477
+ for (const slot of ret.slots) {
2478
+ slotSink[slotKey("return", slot.position, slot.name)] = slot.ts;
2479
+ }
2480
+ }
2481
+ }
2482
+ if (isPredicate && soleParam !== undefined && predicateToken !== undefined) {
2250
2483
  return `${name}(${params}): ${safeParamName(soleParam.name, 0)} is ${mapType(predicateToken)};`;
2251
2484
  }
2252
2485
  return `${name}(${params}): ${ret.type};${ret.trailing}`;
@@ -2259,8 +2492,9 @@ function emitFunction(
2259
2492
  resolver: TableDocResolver,
2260
2493
  constantTokens: ConstantSlotTokens,
2261
2494
  urlParameters: UrlParameterTable,
2495
+ slotSink?: Record<string, string>,
2262
2496
  ): string {
2263
- return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters)}`;
2497
+ return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, slotSink)}`;
2264
2498
  }
2265
2499
 
2266
2500
  // A colon-method member of a handle interface: identical signature machinery to a
@@ -2272,8 +2506,17 @@ function emitMethod(
2272
2506
  resolver: TableDocResolver,
2273
2507
  constantTokens: ConstantSlotTokens,
2274
2508
  urlParameters: UrlParameterTable,
2509
+ slotSink?: Record<string, string>,
2275
2510
  ): string {
2276
- return memberSignature(prepared, prepared.name, mapType, resolver, constantTokens, urlParameters);
2511
+ return memberSignature(
2512
+ prepared,
2513
+ prepared.name,
2514
+ mapType,
2515
+ resolver,
2516
+ constantTokens,
2517
+ urlParameters,
2518
+ slotSink,
2519
+ );
2277
2520
  }
2278
2521
 
2279
2522
  // Build the indented JSDoc lines for a function from its ref-doc prose. The
@@ -2444,21 +2687,29 @@ function emitRestParameter(
2444
2687
  constantTokens: ConstantSlotTokens,
2445
2688
  elementName: string,
2446
2689
  urlParameters: UrlParameterTable,
2447
- ): string {
2690
+ ): { text: string; slots: EmittedSlot[] } {
2448
2691
  const vararg = params[varargIndex];
2449
- if (vararg === undefined) return "";
2450
- const members = [
2451
- ...new Set(
2452
- params
2453
- .slice(varargIndex)
2454
- .map((p) =>
2455
- parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters),
2456
- ),
2457
- ),
2458
- ];
2692
+ if (vararg === undefined) return { text: "", slots: [] };
2693
+ const folded = params.slice(varargIndex).map((p, offset) => ({
2694
+ position: varargIndex + offset,
2695
+ name: p.name,
2696
+ ts: parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters),
2697
+ }));
2698
+ const members = [...new Set(folded.map((slot) => slot.ts))];
2459
2699
  const first = members[0] ?? "unknown";
2460
2700
  const element = members.length > 1 ? `(${members.join(" | ")})` : first;
2461
- return `...${varargParamName(vararg.name, varargIndex)}: ${element}[]`;
2701
+ return {
2702
+ text: `...${varargParamName(vararg.name, varargIndex)}: ${element}[]`,
2703
+ slots: folded,
2704
+ };
2705
+ }
2706
+
2707
+ // A slot's rendered type paired with the position and raw ref-doc name that
2708
+ // identify it, so the signature text and the per-slot map come out of one call.
2709
+ interface EmittedSlot {
2710
+ readonly position: number;
2711
+ readonly name: string;
2712
+ readonly ts: string;
2462
2713
  }
2463
2714
 
2464
2715
  function emitParameter(
@@ -2470,7 +2721,7 @@ function emitParameter(
2470
2721
  constantTokens: ConstantSlotTokens,
2471
2722
  elementName: string,
2472
2723
  urlParameters: UrlParameterTable,
2473
- ): string {
2724
+ ): { text: string; ts: string } {
2474
2725
  const name = safeParamName(p.name, index);
2475
2726
  const ts = parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters);
2476
2727
  // An interior doc-optional param (a required param follows, so the trailing-`?`
@@ -2478,7 +2729,7 @@ function emitParameter(
2478
2729
  // lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
2479
2730
  // `?` form; required params are untouched.
2480
2731
  const interiorOptional = !optional && isDocOptional(p, elementName) ? " | undefined" : "";
2481
- return `${name}${optional ? "?" : ""}: ${ts}${interiorOptional}`;
2732
+ return { text: `${name}${optional ? "?" : ""}: ${ts}${interiorOptional}`, ts };
2482
2733
  }
2483
2734
 
2484
2735
  function emitReturn(
@@ -2487,33 +2738,53 @@ function emitReturn(
2487
2738
  resolver: TableDocResolver,
2488
2739
  constantTokens: ConstantSlotTokens,
2489
2740
  elementName: string,
2490
- ): { type: string; trailing: string } {
2741
+ ): { type: string; trailing: string; slots: EmittedSlot[] } {
2742
+ // An authored whole-return override is not a per-slot render, so it reports no
2743
+ // slots and leaves the consumer on its token-derived fallback.
2491
2744
  const override = RETURN_TYPE_OVERRIDES.get(elementName);
2492
- if (override !== undefined) return { type: override, trailing: "" };
2493
- if (returnValues.length === 0) return { type: "void", trailing: "" };
2745
+ if (override !== undefined) return { type: override, trailing: "", slots: [] };
2746
+ // Kept beside the override lookup so the two stay visibly distinct: this one
2747
+ // contradicts a declared token rather than filling an empty `returnvalues`,
2748
+ // and it is a per-slot render, so it reports its slot.
2749
+ const correction = RETURN_TYPE_CORRECTIONS.get(elementName);
2750
+ if (returnValues.length === 0) return { type: "void", trailing: "", slots: [] };
2494
2751
  if (returnValues.length > 1) {
2495
2752
  // Defold multi-returns are positional and always present; each slot maps
2496
2753
  // straight through (unknown when the doc lists no type) into a tuple that
2497
2754
  // typescript-to-lua erases to `local a, b = fn()`.
2498
- const slots = returnValues.map((rv) =>
2499
- rv.types.length > 0
2500
- ? mapSlotUnion(
2501
- rv.types,
2502
- rv.doc,
2503
- mapType,
2504
- false,
2505
- resolver,
2506
- constantTokens,
2507
- elementName,
2508
- "return",
2509
- rv.name,
2510
- )
2511
- : "unknown",
2512
- );
2513
- return { type: `LuaMultiReturn<[${slots.join(", ")}]>`, trailing: "" };
2755
+ const emitted = returnValues.map((rv, index) => ({
2756
+ position: index,
2757
+ name: rv.name,
2758
+ ts:
2759
+ rv.types.length > 0
2760
+ ? mapSlotUnion(
2761
+ rv.types,
2762
+ rv.doc,
2763
+ mapType,
2764
+ false,
2765
+ resolver,
2766
+ constantTokens,
2767
+ elementName,
2768
+ "return",
2769
+ rv.name,
2770
+ )
2771
+ : "unknown",
2772
+ }));
2773
+ return {
2774
+ type: `LuaMultiReturn<[${emitted.map((slot) => slot.ts).join(", ")}]>`,
2775
+ trailing: "",
2776
+ slots: emitted,
2777
+ };
2514
2778
  }
2515
2779
  const first = returnValues[0];
2516
- if (!first) return { type: "void", trailing: "" };
2780
+ if (!first) return { type: "void", trailing: "", slots: [] };
2781
+ if (correction !== undefined) {
2782
+ return {
2783
+ type: correction.ts,
2784
+ trailing: "",
2785
+ slots: [{ position: 0, name: first.name, ts: correction.ts }],
2786
+ };
2787
+ }
2517
2788
  const ts =
2518
2789
  first.types.length > 0
2519
2790
  ? mapSlotUnion(
@@ -2528,7 +2799,7 @@ function emitReturn(
2528
2799
  first.name,
2529
2800
  )
2530
2801
  : "unknown";
2531
- return { type: ts, trailing: "" };
2802
+ return { type: ts, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
2532
2803
  }
2533
2804
 
2534
2805
  function emitPropertyMember(
@@ -2606,7 +2877,14 @@ function mapSlotUnion(
2606
2877
  const nested = mapping.value as NestedMapping;
2607
2878
  value = `LuaMap<${mapType(nested.key)}, ${mapType(nested.value)}>`;
2608
2879
  }
2609
- ts = `LuaMap<${mapType(mapping.key)}, ${value}>`;
2880
+ // A mapping key may be a `A | B` union token too (render.clear's three
2881
+ // documented graphics.BUFFER_TYPE_* constants); split it the same way
2882
+ // the value above is split. A single-token key (no `|`) is unaffected.
2883
+ const key = unionFromTokens(
2884
+ mapping.key.split("|").map((token) => token.trim()),
2885
+ mapType,
2886
+ );
2887
+ ts = `LuaMap<${key}, ${value}>`;
2610
2888
  } else if (element !== undefined) {
2611
2889
  ts = arrayTypeFromTokens(element, mapType);
2612
2890
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {
@@ -2617,19 +2895,25 @@ function mapSlotUnion(
2617
2895
  // arbitrary name. Reusing the parser keeps the inner fields in lockstep
2618
2896
  // with the doc instead of a hand-listed drift-prone copy; a documented
2619
2897
  // array-form field is pinned via TABLE_FIELD_TYPE_OVERRIDES.
2620
- const parsed = applyFieldTypeOverrides(
2898
+ const parsed = applyFieldOptionalityCorrections(
2621
2899
  elementName,
2622
2900
  slotKind,
2623
2901
  slotName,
2624
- parseTableFields(doc, resolver) ?? [],
2902
+ applyFieldTypeOverrides(
2903
+ elementName,
2904
+ slotKind,
2905
+ slotName,
2906
+ parseTableFields(doc, resolver, slotName) ?? [],
2907
+ ),
2625
2908
  );
2626
2909
  const object = inlineTableType(parsed, mapType, optionalFields);
2627
2910
  ts = `Record<string, ${object}>`;
2628
2911
  } else {
2629
- const parsed = parseTableFields(doc, resolver);
2912
+ const parsed = parseTableFields(doc, resolver, slotName);
2630
2913
  if (parsed !== null) {
2631
2914
  const nested = applyNestedFieldCurations(elementName, slotKind, slotName, parsed);
2632
- const fields = applyFieldTypeOverrides(elementName, slotKind, slotName, nested);
2915
+ const typed = applyFieldTypeOverrides(elementName, slotKind, slotName, nested);
2916
+ const fields = applyFieldOptionalityCorrections(elementName, slotKind, slotName, typed);
2633
2917
  const object = inlineTableType(fields, mapType, optionalFields);
2634
2918
  ts = isSlotLevelList(doc) ? `${object}[]` : object;
2635
2919
  } else {
package/src/index.ts CHANGED
@@ -49,6 +49,7 @@ export {
49
49
  type EmitOptions,
50
50
  emitDeclarations,
51
51
  emitSymbolSignatures,
52
+ type SlotTypes,
52
53
  type SymbolSignature,
53
54
  TS_IDENTIFIER,
54
55
  TS_RESERVED_NAMES,