@defold-typescript/types 0.33.0 → 0.33.1

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.
package/src/emit-dts.ts CHANGED
@@ -288,6 +288,69 @@ export const PROPERTY_TYPE_CORRECTIONS: ReadonlyMap<string, PropertyTypeCorrecti
288
288
  ],
289
289
  ]);
290
290
 
291
+ // A parameter the ref-doc metadata marks required while the same element's prose
292
+ // or examples show it being omitted. `is_optional` is the only optionality
293
+ // signal the emitter has, and upstream leaves it `False` on slots its own
294
+ // examples skip — so the emitted declaration rejects the call the docs teach.
295
+ //
296
+ // Keyed `<element>:param:<slot>`, the shape `tableSlotKey` builds, so a
297
+ // same-named slot on another element is untouched. The value is the upstream
298
+ // evidence for that one entry: the example call that omits it, or the doc phrase
299
+ // that names it optional. Both emitted shapes follow from one predicate — a
300
+ // trailing slot becomes `slot?:`, an interior one `slot: T | undefined`.
301
+ //
302
+ // Demonstrated omissions only. `resource.set_texture`'s buffer and
303
+ // `sys.load_buffer_async`'s status_callback read as optional but every upstream
304
+ // example passes them, so neither is a correction candidate.
305
+ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
306
+ [
307
+ "resource.create_texture:param:buffer",
308
+ 'example `resource.create_texture("/my_custom_texture.texturec", tparams)`; "optional buffer of precreated pixel data"',
309
+ ],
310
+ ["resource.create_texture_async:param:buffer", '"optional buffer of precreated pixel data"'],
311
+ [
312
+ "resource.create_texture_async:param:callback",
313
+ 'example `resource.create_texture_async("/my_texture.texturec", tparams, tbuffer)`',
314
+ ],
315
+ [
316
+ "gui.new_texture:param:flip",
317
+ 'example `gui.new_texture("orange_tx", w, h, "rgb", string.rep(orange, w * h))`',
318
+ ],
319
+ [
320
+ "gui.set_texture_data:param:flip",
321
+ 'example `gui.set_texture_data("dynamic_tx", w, h, "rgb", string.rep(orange, w * h))`',
322
+ ],
323
+ ["sys.set_engine_throttle:param:cooldown", "example `sys.set_engine_throttle(false)`"],
324
+ [
325
+ "b2d.fixture.set_shape:param:update_mass",
326
+ '2 of 3 examples omit it; "The body mass is not updated unless update_mass is true"',
327
+ ],
328
+ [
329
+ "b2d.shape.ray_cast:param:max_fraction",
330
+ '"optional maximum translation fraction, defaults to 1"',
331
+ ],
332
+ ["iap.buy:param:options", '"optional parameters as properties"'],
333
+ ...(
334
+ [
335
+ "create_distance",
336
+ "create_mouse",
337
+ "create_prismatic",
338
+ "create_revolute",
339
+ "create_weld",
340
+ "create_wheel",
341
+ "create_friction",
342
+ "create_rope",
343
+ "create_pulley",
344
+ "create_gear",
345
+ "create_motor",
346
+ "create_filter",
347
+ ] as const
348
+ ).map(
349
+ (name) =>
350
+ [`b2d.joint.${name}:param:definition`, '"optional definition"'] as readonly [string, string],
351
+ ),
352
+ ]);
353
+
291
354
  // FQN-keyed allowlist of the `types.is_*` checks that genuinely narrow their
292
355
  // argument, mapped to the `DEFOLD_TYPE_MAP` token whose interface they prove.
293
356
  // Emitting these as user-defined type guards (`var_ is Vector3`) is the only way
@@ -2013,7 +2076,7 @@ function memberSignature(
2013
2076
  ): string {
2014
2077
  const original = prepared.original.parameters;
2015
2078
  const elementName = prepared.original.name;
2016
- const cutoff = trailingOptionalCutoff(original);
2079
+ const cutoff = trailingOptionalCutoff(original, elementName);
2017
2080
  const varargIndex = original.findIndex(isVarargParameter);
2018
2081
  const positional = (varargIndex === -1 ? original : original.slice(0, varargIndex)).map((p, i) =>
2019
2082
  emitParameter(p, i, i >= cutoff, mapType, resolver, elementName, urlParameters),
@@ -2124,15 +2187,31 @@ export function summaryFor(brief: string, description: string): string {
2124
2187
  return description.trim() !== "" ? description : brief;
2125
2188
  }
2126
2189
 
2127
- function isDocOptional(p: ApiParameter): boolean {
2190
+ export function isMarkedOptional(p: ApiParameter): boolean {
2128
2191
  return p.isOptional || p.types.includes("nil");
2129
2192
  }
2130
2193
 
2131
- function trailingOptionalCutoff(params: readonly ApiParameter[]): number {
2194
+ // The one optionality predicate, in two halves: the ref-doc's own marking
2195
+ // (`isMarkedOptional`, which the correction provenance guards read alone to
2196
+ // tell which targets still need a correction) and the OPTIONAL_SLOT_CORRECTIONS
2197
+ // entries that override it. Both emitted shapes read it — the trailing `?`
2198
+ // through `trailingOptionalCutoff` and the interior `| undefined` through
2199
+ // `emitParameter` — so a correction threaded in here reaches both without a
2200
+ // second copy of the rule. The fidelity audit reads it too, to tell which slots
2201
+ // the emitted surface still makes required.
2202
+ export function isDocOptional(p: ApiParameter, elementName: string): boolean {
2203
+ if (isMarkedOptional(p)) return true;
2204
+ return OPTIONAL_SLOT_CORRECTIONS.has(tableSlotKey(elementName, "param", p.name));
2205
+ }
2206
+
2207
+ export function trailingOptionalCutoff(
2208
+ params: readonly ApiParameter[],
2209
+ elementName: string,
2210
+ ): number {
2132
2211
  let cutoff = params.length;
2133
2212
  for (let i = params.length - 1; i >= 0; i -= 1) {
2134
2213
  const p = params[i];
2135
- if (p && isDocOptional(p)) cutoff = i;
2214
+ if (p && isDocOptional(p, elementName)) cutoff = i;
2136
2215
  else break;
2137
2216
  }
2138
2217
  return cutoff;
@@ -2230,7 +2309,7 @@ function emitParameter(
2230
2309
  // projection cannot mark it) keeps its optionality as `| undefined` — TSTL
2231
2310
  // lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
2232
2311
  // `?` form; required params are untouched.
2233
- const interiorOptional = !optional && isDocOptional(p) ? " | undefined" : "";
2312
+ const interiorOptional = !optional && isDocOptional(p, elementName) ? " | undefined" : "";
2234
2313
  return `${name}${optional ? "?" : ""}: ${ts}${interiorOptional}`;
2235
2314
  }
2236
2315
 
@@ -48,6 +48,13 @@ declare global {
48
48
  *
49
49
  * The two-arg form `msg.url(socket, path)` is a runtime error.
50
50
  *
51
+ * `socket` may be `undefined` (Lua `nil`), which addresses the current
52
+ * game world, exactly as the runtime's own examples do.
53
+ *
54
+ * The one-argument form's `urlstring` may likewise be `undefined` (Lua
55
+ * `nil`), which the ref-doc calls equivalent to `msg.url()` — the URL of the
56
+ * current script.
57
+ *
51
58
  * In the same world, address a sibling **relatively** — by bare id
52
59
  * (`msg.url("camera")`), absolute path (`msg.url("/camera")`), or
53
60
  * component (`msg.url("#main")`). The `socket:` prefix only crosses
@@ -58,6 +65,9 @@ declare global {
58
65
  * // No-arg, then take the current game object's URL.
59
66
  * const self: Url = msg.url();
60
67
  *
68
+ * // An `undefined` argument is the no-arg form spelled out.
69
+ * const alsoSelf: Url = msg.url(undefined);
70
+ *
61
71
  * // A sibling in the same world — bare id, no socket prefix.
62
72
  * const sibling: Url = msg.url("camera");
63
73
  *
@@ -69,10 +79,17 @@ declare global {
69
79
  *
70
80
  * // Crossing into a proxy-loaded world — `socket` is the world name.
71
81
  * const proxied: Url = msg.url(hash("level1"), hash("/door"), hash("script"));
82
+ *
83
+ * // An `undefined` socket addresses the current game world.
84
+ * const currentWorld: Url = msg.url(undefined, hash("/door"), hash("script"));
72
85
  * ```
73
86
  */
74
87
  function url(): Url;
75
- function url(urlstring: SceneAddress): Url;
76
- function url(socket: string | Hash, path: string | Hash, fragment: string | Hash): Url;
88
+ function url(urlstring: SceneAddress | undefined): Url;
89
+ function url(
90
+ socket: string | Hash | undefined,
91
+ path: string | Hash,
92
+ fragment: string | Hash,
93
+ ): Url;
77
94
  }
78
95
  }
@@ -0,0 +1,126 @@
1
+ import { type ApiModule, parseDefoldApiDoc } from "./api-doc";
2
+ import { isMarkedOptional } from "./emit-dts";
3
+
4
+ export interface ProvenanceSurface {
5
+ readonly target: string;
6
+ readonly namespace: string;
7
+ readonly doc: unknown;
8
+ }
9
+
10
+ export interface CorrectionProvenance {
11
+ readonly key: string;
12
+ readonly sightedIn: readonly string[];
13
+ readonly neededBy: readonly string[];
14
+ readonly markedIn: readonly string[];
15
+ }
16
+
17
+ interface ManifestEntry {
18
+ readonly namespace: string;
19
+ readonly doc: unknown;
20
+ }
21
+
22
+ interface VersionedManifestEntry extends ManifestEntry {
23
+ readonly versionId: string;
24
+ readonly editor?: boolean;
25
+ }
26
+
27
+ // Every runtime module regen emits through the global correction table: the
28
+ // default target's manifest plus every older generated target's. Editor-VM
29
+ // entries are outside the scope the corrections cover.
30
+ export function retainedSurfaces(
31
+ defaultTarget: string,
32
+ defaultManifest: readonly ManifestEntry[],
33
+ versionedManifest: readonly VersionedManifestEntry[],
34
+ ): ProvenanceSurface[] {
35
+ return [
36
+ ...defaultManifest.map((entry) => ({
37
+ target: defaultTarget,
38
+ namespace: entry.namespace,
39
+ doc: entry.doc,
40
+ })),
41
+ ...versionedManifest
42
+ .filter((entry) => entry.editor !== true)
43
+ .map((entry) => ({ target: entry.versionId, namespace: entry.namespace, doc: entry.doc })),
44
+ ];
45
+ }
46
+
47
+ const PARAM_SEPARATOR = ":param:";
48
+
49
+ // A target needs a correction while any sighting of the slot there is unmarked,
50
+ // so an overload that marks it in one declaration and not another still needs
51
+ // it. The correction can go only once no target needs it.
52
+ export function correctionProvenance(
53
+ keys: readonly string[],
54
+ surfaces: readonly ProvenanceSurface[],
55
+ ): CorrectionProvenance[] {
56
+ const parsed = new Map<ProvenanceSurface, ApiModule>();
57
+ const parse = (surface: ProvenanceSurface): ApiModule => {
58
+ let module = parsed.get(surface);
59
+ if (!module) {
60
+ module = parseDefoldApiDoc(surface.doc);
61
+ parsed.set(surface, module);
62
+ }
63
+ return module;
64
+ };
65
+
66
+ return keys.map((key) => {
67
+ const separator = key.lastIndexOf(PARAM_SEPARATOR);
68
+ if (separator < 0) throw new Error(`correctionProvenance: malformed key ${key}`);
69
+ const element = key.slice(0, separator);
70
+ const slot = key.slice(separator + PARAM_SEPARATOR.length);
71
+ const namespace = element.slice(0, element.lastIndexOf("."));
72
+
73
+ const markedByTarget = new Map<string, boolean>();
74
+ for (const surface of surfaces) {
75
+ if (surface.namespace !== namespace) continue;
76
+ for (const fn of parse(surface).functions) {
77
+ if (fn.name !== element) continue;
78
+ for (const parameter of fn.parameters) {
79
+ if (parameter.name !== slot) continue;
80
+ const marked = markedByTarget.get(surface.target) ?? true;
81
+ markedByTarget.set(surface.target, marked && isMarkedOptional(parameter));
82
+ }
83
+ }
84
+ }
85
+
86
+ const sightedIn = [...markedByTarget.keys()].sort();
87
+ return {
88
+ key,
89
+ sightedIn,
90
+ neededBy: sightedIn.filter((target) => markedByTarget.get(target) === false),
91
+ markedIn: sightedIn.filter((target) => markedByTarget.get(target) === true),
92
+ };
93
+ });
94
+ }
95
+
96
+ // Reads the raw doc rather than `parseDefoldApiDoc`, which folds an absent
97
+ // `is_optional` into `isOptional: false` and so erases the difference between a
98
+ // parameter upstream calls required and one it never classified at all.
99
+ export function whollyUnmarkedNamespaces(
100
+ surfaces: readonly ProvenanceSurface[],
101
+ ): Map<string, string[]> {
102
+ const out = new Map<string, string[]>();
103
+ for (const surface of surfaces) {
104
+ const namespaces = out.get(surface.target) ?? [];
105
+ out.set(surface.target, namespaces);
106
+ const parameters = rawFunctionParameters(surface.doc);
107
+ if (parameters.length > 0 && parameters.every((parameter) => !("is_optional" in parameter))) {
108
+ namespaces.push(surface.namespace);
109
+ }
110
+ }
111
+ for (const namespaces of out.values()) namespaces.sort();
112
+ return out;
113
+ }
114
+
115
+ function rawFunctionParameters(doc: unknown): Record<string, unknown>[] {
116
+ if (!isRecord(doc) || !Array.isArray(doc.elements)) return [];
117
+ return doc.elements.flatMap((element) =>
118
+ isRecord(element) && element.type === "FUNCTION" && Array.isArray(element.parameters)
119
+ ? element.parameters.filter(isRecord)
120
+ : [],
121
+ );
122
+ }
123
+
124
+ function isRecord(value: unknown): value is Record<string, unknown> {
125
+ return typeof value === "object" && value !== null && !Array.isArray(value);
126
+ }