@defold-typescript/types 0.33.1 → 0.35.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 (64) hide show
  1. package/api-signatures.json +156 -183
  2. package/api-targets.json +9 -66
  3. package/generated/buffer.d.ts +2 -2
  4. package/generated/collectionfactory.d.ts +2 -2
  5. package/generated/factory.d.ts +2 -2
  6. package/generated/gui.d.ts +23 -23
  7. package/generated/kinds/gui-script.d.ts +0 -4
  8. package/generated/kinds/render-script.d.ts +1 -4
  9. package/generated/kinds/script.d.ts +0 -4
  10. package/generated/model.d.ts +2 -2
  11. package/generated/profiler.d.ts +2 -4
  12. package/generated/render.d.ts +2 -150
  13. package/generated/sys.d.ts +1 -1
  14. package/generated/versions/defold-1.12.4/buffer.d.ts +2 -2
  15. package/generated/versions/defold-1.12.4/collectionfactory.d.ts +2 -2
  16. package/generated/versions/defold-1.12.4/factory.d.ts +2 -2
  17. package/generated/versions/defold-1.12.4/gui.d.ts +23 -23
  18. package/generated/versions/defold-1.12.4/index.d.ts +9 -4
  19. package/generated/versions/defold-1.12.4/model.d.ts +2 -2
  20. package/generated/versions/defold-1.12.4/profiler.d.ts +2 -4
  21. package/generated/versions/defold-1.12.4/render.d.ts +2 -150
  22. package/generated/versions/defold-1.12.4/sys.d.ts +1 -1
  23. package/generated/versions/defold-1.12.4/vmath.d.ts +0 -19
  24. package/generated/versions/defold-1.12.4/window.d.ts +2 -4
  25. package/generated/versions/defold-1.13.0/buffer.d.ts +2 -2
  26. package/generated/versions/defold-1.13.0/collectionfactory.d.ts +2 -2
  27. package/generated/versions/defold-1.13.0/factory.d.ts +2 -2
  28. package/generated/versions/defold-1.13.0/gui.d.ts +23 -23
  29. package/generated/versions/defold-1.13.0/index.d.ts +9 -4
  30. package/generated/versions/defold-1.13.0/model.d.ts +2 -2
  31. package/generated/versions/defold-1.13.0/profiler.d.ts +2 -4
  32. package/generated/versions/defold-1.13.0/render.d.ts +2 -150
  33. package/generated/versions/defold-1.13.0/sys.d.ts +1 -1
  34. package/generated/versions/defold-1.13.0/vmath.d.ts +0 -19
  35. package/generated/versions/defold-1.13.0/window.d.ts +2 -4
  36. package/generated/vmath.d.ts +0 -19
  37. package/generated/window.d.ts +2 -4
  38. package/index.d.ts +1 -4
  39. package/package.json +4 -2
  40. package/scripts/extension-goldens.ts +23 -0
  41. package/scripts/generate-api-signatures.ts +40 -1
  42. package/scripts/materialize-version.ts +15 -1
  43. package/scripts/regen.ts +109 -27
  44. package/scripts/signature-store-fs.ts +1 -0
  45. package/scripts/sync-api-docs.ts +11 -61
  46. package/src/doc-comment.ts +10 -6
  47. package/src/emit-dts.ts +212 -21
  48. package/src/optional-correction-provenance.ts +13 -2
  49. package/src/render-overloads.d.ts +29 -0
  50. package/src/script-api.ts +154 -23
  51. package/src/vmath-overloads.d.ts +19 -0
  52. package/generated/iac.d.ts +0 -18
  53. package/generated/iap.d.ts +0 -60
  54. package/generated/push.d.ts +0 -66
  55. package/generated/versions/defold-1.12.4/iac.d.ts +0 -18
  56. package/generated/versions/defold-1.12.4/iap.d.ts +0 -60
  57. package/generated/versions/defold-1.12.4/push.d.ts +0 -66
  58. package/generated/versions/defold-1.12.4/webview.d.ts +0 -68
  59. package/generated/versions/defold-1.13.0/iac.d.ts +0 -18
  60. package/generated/versions/defold-1.13.0/iap.d.ts +0 -60
  61. package/generated/versions/defold-1.13.0/push.d.ts +0 -66
  62. package/generated/versions/defold-1.13.0/webview.d.ts +0 -68
  63. package/generated/webview.d.ts +0 -68
  64. package/scripts/fidelity-audit.ts +0 -684
@@ -1,684 +0,0 @@
1
- import { resolve } from "node:path";
2
- import { type ApiFunction, parseDefoldApiDoc } from "../src/api-doc";
3
- import { DEFOLD_TYPE_MAP } from "../src/core-types";
4
- import {
5
- ARBITRARY_TABLE_SLOT_KEYS,
6
- ARBITRARY_TABLE_SLOTS,
7
- applyFieldTypeOverrides,
8
- applyNestedFieldCurations,
9
- buildTableDocResolver,
10
- HANDLE_METHOD_LOCAL,
11
- HOMOGENEOUS_ARRAY_SLOTS,
12
- isDocOptional,
13
- isRecordsCollectionSlot,
14
- isSlotLevelList,
15
- MAPPING_TABLE_SLOTS,
16
- type NestedMapping,
17
- OVERLOAD_COVERED_SKIPS,
18
- parseTableFields,
19
- recoverCallbackSignature,
20
- TABLE_SLOT_CURATIONS,
21
- type TableSlotCuration,
22
- TS_IDENTIFIER,
23
- trailingOptionalCutoff,
24
- } from "../src/emit-dts";
25
- import { parseMessagesDoc } from "../src/emit-messages";
26
- import {
27
- collectConstantFqns,
28
- FIDELITY_BASELINE_MANIFEST,
29
- generateModuleDeclaration,
30
- MESSAGES_MANIFEST,
31
- type ModuleManifestEntry,
32
- } from "./regen";
33
-
34
- const NO_KNOWN_CONSTANTS: ReadonlySet<string> = new Set();
35
-
36
- // These message names are typed by builtin-messages-typing's separate surface
37
- // (BuiltinMessages), so a fixture `MESSAGE` element of the same name is not a
38
- // namespace-API loss — it is reclassified out of droppedElements below.
39
- const BUILTIN_MESSAGE_NAMES: ReadonlySet<string> = new Set(
40
- parseMessagesDoc(MESSAGES_MANIFEST.doc).entries.map((e) => e.name),
41
- );
42
-
43
- export interface FidelityEntry {
44
- droppedElements: number;
45
- unknownTokens: string[];
46
- recordTables: number;
47
- multiReturn: number;
48
- droppedMembers: number;
49
- optionalAsRequired: number;
50
- }
51
-
52
- function isRecord(value: unknown): value is Record<string, unknown> {
53
- return typeof value === "object" && value !== null && !Array.isArray(value);
54
- }
55
-
56
- function elementsOf(doc: unknown): Record<string, unknown>[] {
57
- if (!isRecord(doc) || !Array.isArray(doc.elements)) return [];
58
- return doc.elements.filter(isRecord);
59
- }
60
-
61
- function stringArray(raw: unknown): string[] {
62
- return Array.isArray(raw) ? raw.filter((item): item is string => typeof item === "string") : [];
63
- }
64
-
65
- function paramList(raw: unknown): Record<string, unknown>[] {
66
- return Array.isArray(raw) ? raw.filter(isRecord) : [];
67
- }
68
-
69
- function docString(raw: unknown): string | undefined {
70
- return typeof raw === "string" ? raw : undefined;
71
- }
72
-
73
- // Colon `<receiver>:<method>` FUNCTION elements are emitted as method-bearing
74
- // interfaces (see emit-dts collectHandleMethodGroups). Before that recovery they
75
- // emitted nothing yet were never counted as a droppedMembers loss — a blind spot
76
- // that let `socket` read `droppedMembers: 0` over an incomplete surface. Count
77
- // them honestly: with emission on, only a malformed colon name (non-identifier
78
- // segment) is still a loss; with emission off, every colon method is dropped.
79
- export function countDroppedHandleMethods(doc: unknown, namespace: string, emit = true): number {
80
- const prefix = `${namespace}.`;
81
- let dropped = 0;
82
- for (const element of elementsOf(doc)) {
83
- if (element.type !== "FUNCTION" || typeof element.name !== "string") continue;
84
- const local = element.name.startsWith(prefix)
85
- ? element.name.slice(prefix.length)
86
- : element.name;
87
- if (!local.includes(":")) continue;
88
- if (!emit || !HANDLE_METHOD_LOCAL.test(local)) dropped += 1;
89
- }
90
- return dropped;
91
- }
92
-
93
- // Upstream ships `examples`, `description` and `brief` as syntax-highlighted HTML
94
- // — every token in its own span, quotes as entities — so a call site is invisible
95
- // to any parser until the markup is gone. `&amp;` decodes last so an escaped
96
- // entity stays literal.
97
- function plainText(html: string): string {
98
- return html
99
- .replace(/<[^>]*>/g, "")
100
- .replace(/&quot;/g, '"')
101
- .replace(/&#39;/g, "'")
102
- .replace(/&lt;/g, "<")
103
- .replace(/&gt;/g, ">")
104
- .replace(/&amp;/g, "&");
105
- }
106
-
107
- // The arguments of the call whose `(` ends just before `start`, split on
108
- // top-level commas only: a comma inside a nested call, table, index or string
109
- // literal belongs to that argument. Null when the text ends before the call does.
110
- function splitArguments(text: string, start: number): string[] | null {
111
- const args: string[] = [];
112
- let current = "";
113
- let depth = 0;
114
- let quote: string | null = null;
115
- for (let i = start; i < text.length; i += 1) {
116
- const ch = text[i] as string;
117
- if (quote !== null) {
118
- current += ch;
119
- if (ch === "\\") {
120
- current += text[i + 1] ?? "";
121
- i += 1;
122
- } else if (ch === quote) {
123
- quote = null;
124
- }
125
- continue;
126
- }
127
- if (ch === '"' || ch === "'") {
128
- quote = ch;
129
- } else if (ch === "(" || ch === "{" || ch === "[") {
130
- depth += 1;
131
- } else if (ch === ")" || ch === "}" || ch === "]") {
132
- if (depth === 0) {
133
- if (ch !== ")") return null;
134
- const last = current.trim();
135
- if (last !== "" || args.length > 0) args.push(last);
136
- return args;
137
- }
138
- depth -= 1;
139
- } else if (ch === "," && depth === 0) {
140
- args.push(current.trim());
141
- current = "";
142
- continue;
143
- }
144
- current += ch;
145
- }
146
- return null;
147
- }
148
-
149
- // Inline `<code>` in example prose names a function rather than calling it
150
- // (`<code>gui.set_texture()</code> is sufficient`). A highlighted block's `<code>`
151
- // always wraps token spans, so only the prose kind is bare text.
152
- const INLINE_CODE = /<code>[^<]*<\/code>/g;
153
-
154
- const exampleCallCache = new WeakMap<ApiFunction, readonly (readonly string[])[]>();
155
-
156
- // Every `<fqn>(…)` call in the function's upstream examples, as argument lists.
157
- function exampleCalls(fn: ApiFunction): readonly (readonly string[])[] {
158
- const cached = exampleCallCache.get(fn);
159
- if (cached !== undefined) return cached;
160
- const text = plainText((fn.examples ?? "").replace(INLINE_CODE, ""));
161
- const escaped = fn.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
162
- const calls: string[][] = [];
163
- for (const match of text.matchAll(new RegExp(`(?<![\\w.:])${escaped}\\s*\\(`, "g"))) {
164
- const args = splitArguments(text, match.index + match[0].length);
165
- if (args !== null) calls.push(args);
166
- }
167
- exampleCallCache.set(fn, calls);
168
- return calls;
169
- }
170
-
171
- // Whether a call passing `count` arguments fits this declaration's arity, with
172
- // the emitter's own trailing-optional cutoff as the floor.
173
- function acceptsArgumentCount(fn: ApiFunction, count: number): boolean {
174
- const params = fn.parameters;
175
- if (count < trailingOptionalCutoff(params, fn.name)) return false;
176
- return count <= params.length || params.some((p) => p.isVararg === true);
177
- }
178
-
179
- // The ref-doc documents an overload as a separate element sharing the name, and
180
- // an element's examples often call a sibling (`vmath.vector3(2.0)` beside the
181
- // `x, y, z` form). A call another same-named declaration accepts is that
182
- // declaration's call, not an omission from this one.
183
- function ownExampleCalls(
184
- fn: ApiFunction,
185
- overloads: readonly ApiFunction[],
186
- ): readonly (readonly string[])[] {
187
- return exampleCalls(fn).filter(
188
- (args) => !overloads.some((other) => other !== fn && acceptsArgumentCount(other, args.length)),
189
- );
190
- }
191
-
192
- // A table parameter's `<dl>`/`<ul>` describes its fields, so an optional field
193
- // there says nothing about whether the argument itself may be omitted.
194
- const FIELD_LIST = /<(dl|ul|ol|table)\b[\s\S]*?<\/\1>/g;
195
- const OPENS_OPTIONAL = /^\s*optional\b/i;
196
- const OMISSION_PHRASE = /\bdefaults? to\b|\b(?:if|when|can be|may be) omitted\b/i;
197
-
198
- type OptionalityAxis = (
199
- fn: ApiFunction,
200
- index: number,
201
- overloads: readonly ApiFunction[],
202
- ) => boolean;
203
-
204
- // The three ways a ref-doc evidences that a parameter may be omitted, each read
205
- // from one function element and its same-named overloads. `is_optional` is not
206
- // among them: a slot upstream marks optional is already emitted omissible, so
207
- // only the unmarked ones can be lost.
208
- export const OPTIONALITY_EVIDENCE: Readonly<
209
- Record<"prose" | "exampleArity" | "exampleNil", OptionalityAxis>
210
- > = {
211
- // The parameter's own prose opens with "optional", or names a default or an
212
- // omission outside any field list.
213
- prose: (fn, index) => {
214
- const doc = fn.parameters[index]?.doc ?? "";
215
- return (
216
- OPENS_OPTIONAL.test(plainText(doc)) ||
217
- OMISSION_PHRASE.test(plainText(doc.replace(FIELD_LIST, " ")))
218
- );
219
- },
220
- // An upstream example stops before this parameter. A trailing `...` forwards
221
- // an unknown number of values, so it proves nothing about arity.
222
- exampleArity: (fn, index, overloads) =>
223
- ownExampleCalls(fn, overloads).some((args) => args.length <= index && args.at(-1) !== "..."),
224
- // An upstream example passes a bare `nil` in this position.
225
- exampleNil: (fn, index, overloads) =>
226
- ownExampleCalls(fn, overloads).some((args) => args[index] === "nil"),
227
- };
228
-
229
- const ARITY_OVERLOAD_REASON =
230
- "the example `vmath.euler_to_quat(v)` passes one vector3 for all three angles: " +
231
- "y and z are required in the three-number form, and the one-argument call needs " +
232
- "an overload the declaration lacks, not an omissible slot";
233
-
234
- // Slots the evidence flags that are genuinely required, keyed like
235
- // `OPTIONAL_SLOT_CORRECTIONS`. Each value is the recorded reason, so an
236
- // exemption is a decision someone can re-check rather than a silent skip.
237
- export const OPTIONALITY_EVIDENCE_EXEMPTIONS: ReadonlyMap<string, string> = new Map([
238
- [
239
- "render.render_target:param:parameters",
240
- "both upstream examples pass the parameters table first, in place of `name`: an " +
241
- "argument shifted into another slot rather than this one omitted, which an " +
242
- "overload the declaration lacks would express",
243
- ],
244
- ["vmath.euler_to_quat:param:y", ARITY_OVERLOAD_REASON],
245
- ["vmath.euler_to_quat:param:z", ARITY_OVERLOAD_REASON],
246
- ]);
247
-
248
- export interface EvidencedSlot {
249
- readonly key: string;
250
- readonly emittedRequired: boolean;
251
- }
252
-
253
- // Every `<element>:param:<slot>` the ref-doc evidences as omissible, and whether
254
- // the emitted surface still requires it. A slot is emitted omissible when any
255
- // same-named declaration makes that position omissible under `isDocOptional`,
256
- // the emitter's own predicate, corrections included. Skipped functions are
257
- // hand-authored, so their slots are not the generator's to lose.
258
- export function evidencedOptionalSlots(
259
- entry: Pick<ModuleManifestEntry, "doc" | "skipFunctions">,
260
- ): EvidencedSlot[] {
261
- const skipFunctions = new Set(entry.skipFunctions ?? []);
262
- const functions = parseDefoldApiDoc(entry.doc).functions;
263
- const overloadsByName = new Map<string, ApiFunction[]>();
264
- for (const fn of functions) {
265
- overloadsByName.set(fn.name, [...(overloadsByName.get(fn.name) ?? []), fn]);
266
- }
267
- const emittedRequired = new Map<string, boolean>();
268
- for (const fn of functions) {
269
- if (skipFunctions.has(stripNamespace(fn.name))) continue;
270
- const overloads = overloadsByName.get(fn.name) ?? [fn];
271
- fn.parameters.forEach((param, index) => {
272
- if (param.isVararg === true) return;
273
- if (!Object.values(OPTIONALITY_EVIDENCE).some((axis) => axis(fn, index, overloads))) return;
274
- const omissible = overloads.some((other) => {
275
- const slot = other.parameters[index];
276
- return slot?.name === param.name && isDocOptional(slot, other.name);
277
- });
278
- const key = tableSlotKey(fn.name, "param", param.name);
279
- emittedRequired.set(key, (emittedRequired.get(key) ?? false) || !omissible);
280
- });
281
- }
282
- return [...emittedRequired].map(([key, required]) => ({ key, emittedRequired: required }));
283
- }
284
-
285
- // The evidenced slots the emitted declaration still requires, before exemptions.
286
- export function unmarkedOptionalSlots(
287
- entry: Pick<ModuleManifestEntry, "doc" | "skipFunctions">,
288
- ): string[] {
289
- return evidencedOptionalSlots(entry)
290
- .filter((slot) => slot.emittedRequired)
291
- .map((slot) => slot.key);
292
- }
293
-
294
- function auditEntry(
295
- entry: ModuleManifestEntry,
296
- knownConstantFqns: ReadonlySet<string>,
297
- ): FidelityEntry {
298
- const skipFunctions = new Set(entry.skipFunctions ?? []);
299
- const elements = elementsOf(entry.doc);
300
-
301
- let droppedElements = 0;
302
- let recordTables = 0;
303
- // multiReturn is fully recovered: emitReturn emits LuaMultiReturn<[...]> for
304
- // every >1-return function, so no documented multi-return is a loss anymore.
305
- const multiReturn = 0;
306
- // Every param upstream marks optional is recovered: emitParameter expresses an
307
- // interior one as `| undefined` and a trailing one as `?`. What stays lost is a
308
- // slot the ref-doc evidences as omissible without marking it, which neither an
309
- // OPTIONAL_SLOT_CORRECTIONS entry nor a stated exemption accounts for.
310
- const optionalAsRequired = unmarkedOptionalSlots(entry).filter(
311
- (key) => !OPTIONALITY_EVIDENCE_EXEMPTIONS.has(key),
312
- ).length;
313
- const unknown = new Set<string>();
314
-
315
- const constantFqns = new Set<string>();
316
- for (const element of elements) {
317
- if (element.type === "CONSTANT" && typeof element.name === "string") {
318
- constantFqns.add(element.name);
319
- }
320
- }
321
-
322
- // Same module-scoped resolver the emitter builds, so a cross-reference table
323
- // slot is recovered (not counted under recordTables) in lockstep with the
324
- // emitted surface.
325
- const resolver = buildTableDocResolver(
326
- elements
327
- .filter((element) => element.type === "FUNCTION" && typeof element.name === "string")
328
- .map((element) => ({
329
- name: element.name as string,
330
- slots: [...paramList(element.parameters), ...paramList(element.returnvalues)].map(
331
- (slot) => ({ types: stringArray(slot.types), doc: docString(slot.doc) ?? "" }),
332
- ),
333
- })),
334
- );
335
-
336
- const considerTypes = (
337
- types: readonly string[],
338
- doc?: string,
339
- arbitraryTable = false,
340
- mappingSlot?: { key: string; value: string },
341
- homogeneousElement?: string | readonly string[],
342
- tableSlotCuration?: TableSlotCuration,
343
- slot?: { element: string; kind: "param" | "return"; name: string },
344
- ) => {
345
- for (const token of types) {
346
- // A `table` slot whose doc carries a parseable `<dl>` field list is
347
- // recovered by emit-dts into an inline object type, so it no longer
348
- // collapses to `Record`: don't count it under recordTables. Instead feed
349
- // each recovered field's types back through considerTypes so an unmapped
350
- // field token still surfaces under unknownTokens and a nested `table`
351
- // field (no doc → not recovered) still counts under recordTables. This
352
- // keeps the invariant that every loss in the emitted surface is measured.
353
- if (token === "table") {
354
- // A mapping-table slot is recovered by emit-dts into `LuaMap<K, V>` from
355
- // the curated key/value tokens — not a `Record`, so don't count it.
356
- // Feed the curated tokens back through considerTypes so an unmapped one
357
- // still surfaces under unknownTokens (none today: hash/node/vector3 map).
358
- if (tableSlotCuration?.kind === "mapping") {
359
- // A single-token value feeds straight back; an object-valued mapping
360
- // (`LuaMap<K, { … }>`) feeds the key plus each curated field type, the
361
- // same way the object branch does; a nested-mapping value
362
- // (`LuaMap<K, LuaMap<K, V>>`) feeds the outer key plus the inner
363
- // key/value tokens — so an unmapped token in any arm still surfaces.
364
- if (typeof tableSlotCuration.value === "string") {
365
- // The value may be a `T | U` union token (render.clear's
366
- // `number | vector4`); split on `|` exactly as the emit branch does
367
- // so each token is checked against DEFOLD_TYPE_MAP individually and a
368
- // single-token value is unaffected.
369
- considerTypes([
370
- tableSlotCuration.key,
371
- ...tableSlotCuration.value.split("|").map((token) => token.trim()),
372
- ]);
373
- } else if (Array.isArray(tableSlotCuration.value)) {
374
- considerTypes([tableSlotCuration.key]);
375
- for (const field of tableSlotCuration.value) {
376
- if (field.fields !== undefined) {
377
- for (const nested of field.fields) {
378
- if (nested.numberList === true || nested.tsType !== undefined) continue;
379
- considerTypes(nested.types);
380
- }
381
- } else if (field.numberList !== true && field.tsType === undefined) {
382
- considerTypes(field.types);
383
- }
384
- }
385
- } else {
386
- const nested = tableSlotCuration.value as NestedMapping;
387
- considerTypes([tableSlotCuration.key, nested.key, nested.value]);
388
- }
389
- continue;
390
- }
391
- if (mappingSlot !== undefined) {
392
- considerTypes([mappingSlot.key, mappingSlot.value]);
393
- continue;
394
- }
395
- // A homogeneous-array slot is recovered by emit-dts into `T[]` (or a
396
- // `(A | B)[]` union element) from the curated element token(s) — not a
397
- // `Record`, so don't count it. Feed the curated token(s) back through
398
- // considerTypes so an unmapped one still surfaces under unknownTokens
399
- // (none today: number/hash/string/url map).
400
- if (tableSlotCuration?.kind === "array") {
401
- considerTypes(
402
- typeof tableSlotCuration.element === "string"
403
- ? [tableSlotCuration.element]
404
- : tableSlotCuration.element,
405
- );
406
- continue;
407
- }
408
- if (tableSlotCuration?.kind === "object" || tableSlotCuration?.kind === "array-object") {
409
- for (const field of tableSlotCuration.fields) {
410
- if (field.fields !== undefined) {
411
- for (const nested of field.fields) {
412
- if (nested.numberList === true || nested.tsType !== undefined) continue;
413
- considerTypes(nested.types);
414
- }
415
- } else if (field.numberList !== true && field.tsType === undefined) {
416
- considerTypes(field.types);
417
- }
418
- }
419
- continue;
420
- }
421
- // A keyed-object slot is recovered by emit-dts into `Record<string, { … }>`
422
- // where the inner fields are parser-recovered — not a bare `Record`, so
423
- // don't count it. Feed the parsed inner field types back through
424
- // considerTypes so an unmapped inner token still surfaces under
425
- // unknownTokens (mirrors the parser-recovered branch below).
426
- if (tableSlotCuration?.kind === "keyed-object") {
427
- const rawParsed = doc !== undefined ? parseTableFields(doc, resolver) : null;
428
- if (rawParsed !== null && slot !== undefined) {
429
- const parsed = applyFieldTypeOverrides(slot.element, slot.kind, slot.name, rawParsed);
430
- for (const field of parsed) {
431
- if (field.fields !== undefined) {
432
- for (const nested of field.fields) {
433
- if (nested.numberList === true || nested.tsType !== undefined) continue;
434
- considerTypes(nested.types);
435
- }
436
- } else if (field.numberList !== true && field.tsType === undefined) {
437
- considerTypes(field.types);
438
- }
439
- }
440
- }
441
- continue;
442
- }
443
- if (homogeneousElement !== undefined) {
444
- considerTypes(
445
- typeof homogeneousElement === "string" ? [homogeneousElement] : homogeneousElement,
446
- );
447
- continue;
448
- }
449
- const parsed = doc !== undefined ? parseTableFields(doc, resolver) : null;
450
- const fields =
451
- parsed !== null && slot !== undefined
452
- ? applyFieldTypeOverrides(
453
- slot.element,
454
- slot.kind,
455
- slot.name,
456
- applyNestedFieldCurations(slot.element, slot.kind, slot.name, parsed),
457
- )
458
- : parsed;
459
- if (fields !== null) {
460
- // A recovered field carrying nested fields (the mixed `<dl>`+`<ul>`
461
- // shape, or an injected nested-field curation) emits a nested object,
462
- // not a `Record` — recurse into the nested field types instead of
463
- // counting the nested `table`. A nested number-list member is recovered
464
- // as `number[]`, so skip it the same way the top-level branch does.
465
- // The function-level arbitraryTable / mappingSlot / homogeneousElement
466
- // flags propagate to the recursion so a slot on an ARBITRARY_TABLE_SLOTS
467
- // element (or any other function-level reclassification) does not
468
- // re-count its parsed sub-tables. The parser-recovered field has no
469
- // tableSlotCuration, so the per-slot curation lookup is irrelevant
470
- // here.
471
- for (const field of fields) {
472
- if (field.fields !== undefined) {
473
- for (const nested of field.fields) {
474
- if (nested.numberList === true || nested.tsType !== undefined) continue;
475
- considerTypes(nested.types, undefined, arbitraryTable);
476
- }
477
- } else if (field.numberList === true || field.tsType !== undefined) {
478
- // Recovered as `number[]` (numberList) or pinned via a field-type
479
- // override (tsType) by inlineTableType — no longer a `Record`, so
480
- // skip it. Re-counting its `table` token here would double-count a
481
- // slot the emitted surface no longer loses.
482
- } else {
483
- considerTypes(field.types, undefined, arbitraryTable);
484
- }
485
- }
486
- // A records-collection return slot ("… of tables" / "table of tables")
487
- // whose inner record the parser recovered but the emitter did not wrap
488
- // (isSlotLevelList false) still lost its outer array layer — the emitted
489
- // shape is a bare object. The oracle (isRecordsCollectionSlot) is
490
- // intentionally broader than the emitter's wrap trigger (isSlotLevelList):
491
- // a records-collection wording the trigger stops recognizing re-surfaces
492
- // here as a counted loss, keeping the gate coupled to the emitted surface.
493
- // array / array-object / mapping / homogeneous curations already
494
- // `continue`d above, so reaching here means none applied.
495
- if (
496
- slot?.kind === "return" &&
497
- doc !== undefined &&
498
- isRecordsCollectionSlot(doc) &&
499
- !isSlotLevelList(doc)
500
- ) {
501
- recordTables += 1;
502
- }
503
- continue;
504
- }
505
- // A slot on a serialization/JSON passthrough function is a genuinely
506
- // arbitrary lua table — its emitted `Record<string | number, unknown>`
507
- // is faithful, not a loss — so it is not counted under recordTables.
508
- if (arbitraryTable) continue;
509
- // A per-slot arbitrary allowlist entry (an undocumented table slot on an
510
- // otherwise-documented element) is faithful `Record` too — not a loss.
511
- if (
512
- slot !== undefined &&
513
- ARBITRARY_TABLE_SLOT_KEYS.has(`${slot.element}:${slot.kind}:${slot.name}`)
514
- ) {
515
- continue;
516
- }
517
- recordTables += 1;
518
- continue;
519
- }
520
- // `nil` is the optional-parameter sentinel (emitParameter strips it), not a
521
- // real Defold type the mapper fails to resolve — the optionalAsRequired
522
- // category covers that loss instead. Constant FQNs defined in this module
523
- // (constantFqns) or in any other manifest module (knownConstantFqns) now
524
- // resolve to their brand type, and `function(...)` callback signatures
525
- // recover to typed functions, so none is an unknown token.
526
- if (
527
- token !== "nil" &&
528
- !Object.hasOwn(DEFOLD_TYPE_MAP, token) &&
529
- !constantFqns.has(token) &&
530
- !knownConstantFqns.has(token) &&
531
- recoverCallbackSignature(token) === null
532
- ) {
533
- unknown.add(token);
534
- }
535
- }
536
- };
537
-
538
- for (const element of elements) {
539
- const type = element.type;
540
- // An identifier-named TYPEDEF is recovered into a per-namespace
541
- // `type <name> = Opaque<"<name>">` alias, under the same TS_IDENTIFIER guard
542
- // the emitter uses, so the gate and the emitted surface agree. A
543
- // non-identifier TYPEDEF cannot become an alias and stays dropped.
544
- if (
545
- type === "TYPEDEF" &&
546
- typeof element.name === "string" &&
547
- TS_IDENTIFIER.test(element.name)
548
- ) {
549
- continue;
550
- }
551
- // A MESSAGE element whose name is in the builtin-messages catalog is a
552
- // built-in message owned by builtin-messages-typing, not a namespace-API
553
- // member — counting it here double-counts a surface that is fully typed
554
- // elsewhere. A MESSAGE name absent from the catalog falls through to the
555
- // catch-all and still counts, so the gate stays honest.
556
- if (
557
- type === "MESSAGE" &&
558
- typeof element.name === "string" &&
559
- BUILTIN_MESSAGE_NAMES.has(element.name)
560
- ) {
561
- continue;
562
- }
563
- if (type !== "FUNCTION" && type !== "VARIABLE" && type !== "CONSTANT" && type !== "PROPERTY") {
564
- droppedElements += 1;
565
- continue;
566
- }
567
- // PROPERTY is recovered into the per-namespace `interface properties` block;
568
- // its name+type survive, so it is no longer a dropped element.
569
- if (type === "CONSTANT" || type === "PROPERTY") continue;
570
- if (type === "VARIABLE") {
571
- considerTypes(stringArray(element.types));
572
- continue;
573
- }
574
- // Skipped functions are measured as droppedMembers only; counting their
575
- // params/returns here would double-count losses covered by hand-written d.ts.
576
- if (typeof element.name === "string" && skipFunctions.has(stripNamespace(element.name))) {
577
- continue;
578
- }
579
- const params = paramList(element.parameters);
580
- const returns = paramList(element.returnvalues);
581
- const arbitraryTable =
582
- typeof element.name === "string" && ARBITRARY_TABLE_SLOTS.has(element.name);
583
- const mappingSlot =
584
- typeof element.name === "string" ? MAPPING_TABLE_SLOTS.get(element.name) : undefined;
585
- const homogeneousElement =
586
- typeof element.name === "string" ? HOMOGENEOUS_ARRAY_SLOTS.get(element.name) : undefined;
587
- params.forEach((param) => {
588
- const tableSlotCuration =
589
- typeof element.name === "string" && typeof param.name === "string"
590
- ? TABLE_SLOT_CURATIONS.get(tableSlotKey(element.name, "param", param.name))
591
- : undefined;
592
- considerTypes(
593
- stringArray(param.types),
594
- docString(param.doc),
595
- arbitraryTable,
596
- mappingSlot,
597
- homogeneousElement,
598
- tableSlotCuration,
599
- typeof element.name === "string" && typeof param.name === "string"
600
- ? { element: element.name, kind: "param", name: param.name }
601
- : undefined,
602
- );
603
- // Interior doc-optional params (a required param follows) are no longer a
604
- // loss: emitParameter expresses them as `| undefined`, mirroring the
605
- // emitted surface. optionalAsRequired counts the unmarked ones instead,
606
- // through unmarkedOptionalSlots above.
607
- });
608
- for (const ret of returns) {
609
- const tableSlotCuration =
610
- typeof element.name === "string" && typeof ret.name === "string"
611
- ? TABLE_SLOT_CURATIONS.get(tableSlotKey(element.name, "return", ret.name))
612
- : undefined;
613
- considerTypes(
614
- stringArray(ret.types),
615
- docString(ret.doc),
616
- arbitraryTable,
617
- mappingSlot,
618
- homogeneousElement,
619
- tableSlotCuration,
620
- typeof element.name === "string" && typeof ret.name === "string"
621
- ? { element: element.name, kind: "return", name: ret.name }
622
- : undefined,
623
- );
624
- }
625
- }
626
-
627
- return {
628
- droppedElements,
629
- unknownTokens: [...unknown].sort(),
630
- recordTables,
631
- multiReturn,
632
- droppedMembers:
633
- generateModuleDeclaration(entry, {
634
- knownConstantFqns: NO_KNOWN_CONSTANTS,
635
- }).dropped.filter((name) => !OVERLOAD_COVERED_SKIPS.has(name)).length +
636
- countDroppedHandleMethods(entry.doc, entry.namespace),
637
- optionalAsRequired,
638
- };
639
- }
640
-
641
- function stripNamespace(name: string): string {
642
- const index = name.lastIndexOf(".");
643
- return index === -1 ? name : name.slice(index + 1);
644
- }
645
-
646
- function tableSlotKey(elementName: string, slotKind: "param" | "return", slotName: string): string {
647
- return `${elementName}:${slotKind}:${slotName}`;
648
- }
649
-
650
- export function buildFidelityReport(
651
- manifest: readonly ModuleManifestEntry[] = FIDELITY_BASELINE_MANIFEST,
652
- ): Record<string, FidelityEntry> {
653
- const report: Record<string, FidelityEntry> = {};
654
- const knownConstantFqns = collectConstantFqns(manifest);
655
- for (const namespace of [...manifest.map((e) => e.namespace)].sort()) {
656
- const entry = manifest.find((e) => e.namespace === namespace);
657
- if (entry) report[namespace] = auditEntry(entry, knownConstantFqns);
658
- }
659
- return report;
660
- }
661
-
662
- function biomeFormatJson(raw: string): string {
663
- const out = Bun.spawnSync(
664
- ["bunx", "biome", "format", "--stdin-file-path=fidelity-baseline.json"],
665
- {
666
- stdin: Buffer.from(raw),
667
- },
668
- );
669
- if (out.exitCode !== 0) {
670
- throw new Error(`biome format failed: ${out.stderr.toString()}`);
671
- }
672
- return out.stdout.toString();
673
- }
674
-
675
- if (import.meta.main) {
676
- const report = buildFidelityReport();
677
- if (process.argv.includes("--write")) {
678
- const path = resolve(import.meta.dir, "fidelity-baseline.json");
679
- Bun.write(path, biomeFormatJson(JSON.stringify(report)));
680
- console.log(`wrote ${path}`);
681
- } else {
682
- console.log(JSON.stringify(report, null, 2));
683
- }
684
- }