@defold-typescript/types 0.39.0 → 0.41.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.
- package/api-availability.json +4024 -716
- package/api-migrations.json +27 -0
- package/api-signatures.json +18118 -10264
- package/api-targets.json +448 -11
- package/examples/translations.json +619 -148
- package/fixtures/messages_doc.json +96 -0
- package/generated/b2d.d.ts +680 -4
- package/generated/b2d_body.d.ts +29 -103
- package/generated/b2d_chain.d.ts +5 -6
- package/generated/b2d_fixture.d.ts +34 -38
- package/generated/b2d_joint.d.ts +503 -25
- package/generated/b2d_shape.d.ts +199 -14
- package/generated/b2d_world.d.ts +57 -63
- package/generated/buffer.d.ts +33 -29
- package/generated/builtin-messages.d.ts +7 -1
- package/generated/bullet3d.d.ts +91 -0
- package/generated/bullet3d_collision_object.d.ts +433 -0
- package/generated/bullet3d_constraint.d.ts +818 -0
- package/generated/bullet3d_rigid_body.d.ts +429 -0
- package/generated/bullet3d_shape.d.ts +123 -0
- package/generated/bullet3d_world.d.ts +492 -0
- package/generated/camera.d.ts +31 -34
- package/generated/collectionfactory.d.ts +26 -27
- package/generated/collectionproxy.d.ts +22 -6
- package/generated/compute.d.ts +12 -238
- package/generated/crash.d.ts +24 -10
- package/generated/editor-vm/http.d.ts +90 -7
- package/generated/editor-vm/image.d.ts +7 -7
- package/generated/editor-vm/json.d.ts +11 -0
- package/generated/editor-vm/localization.d.ts +7 -7
- package/generated/editor-vm/tilemap_tiles.d.ts +56 -10
- package/generated/editor-vm/zip.d.ts +28 -0
- package/generated/editor.d.ts +1496 -430
- package/generated/factory.d.ts +21 -25
- package/generated/font.d.ts +64 -22
- package/generated/go.d.ts +243 -186
- package/generated/graphics.d.ts +374 -84
- package/generated/gui.d.ts +282 -492
- package/generated/html5.d.ts +5 -3
- package/generated/http.d.ts +31 -36
- package/generated/image.d.ts +127 -85
- package/generated/json.d.ts +34 -16
- package/generated/kinds/gui-script.d.ts +8 -0
- package/generated/kinds/render-script.d.ts +8 -0
- package/generated/kinds/script.d.ts +8 -0
- package/generated/label.d.ts +77 -7
- package/generated/liveupdate.d.ts +13 -10
- package/generated/material.d.ts +312 -360
- package/generated/model.d.ts +72 -42
- package/generated/particlefx.d.ts +19 -28
- package/generated/physics.d.ts +378 -144
- package/generated/profiler.d.ts +14 -24
- package/generated/render.d.ts +294 -297
- package/generated/resource.d.ts +753 -641
- package/generated/socket.d.ts +131 -134
- package/generated/sound.d.ts +65 -34
- package/generated/sprite.d.ts +39 -17
- package/generated/sys.d.ts +237 -182
- package/generated/tilemap.d.ts +21 -16
- package/generated/timer.d.ts +34 -21
- package/generated/versions/defold-1.12.4/b2d_body.d.ts +0 -7
- package/generated/versions/defold-1.12.4/buffer.d.ts +18 -17
- package/generated/versions/defold-1.12.4/camera.d.ts +23 -23
- package/generated/versions/defold-1.12.4/collectionfactory.d.ts +21 -19
- package/generated/versions/defold-1.12.4/collectionproxy.d.ts +2 -2
- package/generated/versions/defold-1.12.4/crash.d.ts +2 -2
- package/generated/versions/defold-1.12.4/factory.d.ts +15 -16
- package/generated/versions/defold-1.12.4/go.d.ts +15 -17
- package/generated/versions/defold-1.12.4/gui.d.ts +111 -193
- package/generated/versions/defold-1.12.4/html5.d.ts +4 -2
- package/generated/versions/defold-1.12.4/http.d.ts +4 -15
- package/generated/versions/defold-1.12.4/image.d.ts +38 -46
- package/generated/versions/defold-1.12.4/index.d.ts +2 -0
- package/generated/versions/defold-1.12.4/json.d.ts +10 -6
- package/generated/versions/defold-1.12.4/label.d.ts +2 -3
- package/generated/versions/defold-1.12.4/liveupdate.d.ts +5 -3
- package/generated/versions/defold-1.12.4/model.d.ts +34 -13
- package/generated/versions/defold-1.12.4/particlefx.d.ts +2 -5
- package/generated/versions/defold-1.12.4/physics.d.ts +82 -78
- package/generated/versions/defold-1.12.4/profiler.d.ts +3 -6
- package/generated/versions/defold-1.12.4/render.d.ts +160 -132
- package/generated/versions/defold-1.12.4/resource.d.ts +212 -209
- package/generated/versions/defold-1.12.4/socket.d.ts +45 -61
- package/generated/versions/defold-1.12.4/sound.d.ts +11 -11
- package/generated/versions/defold-1.12.4/sprite.d.ts +24 -6
- package/generated/versions/defold-1.12.4/sys.d.ts +45 -68
- package/generated/versions/defold-1.12.4/tilemap.d.ts +18 -11
- package/generated/versions/defold-1.12.4/timer.d.ts +5 -3
- package/generated/versions/defold-1.12.4/vmath.d.ts +5 -26
- package/generated/versions/defold-1.12.4/window.d.ts +1 -16
- package/generated/versions/defold-1.13.0/b2d_body.d.ts +10 -55
- package/generated/versions/defold-1.13.0/b2d_fixture.d.ts +25 -25
- package/generated/versions/defold-1.13.0/b2d_shape.d.ts +188 -5
- package/generated/versions/defold-1.13.0/b2d_world.d.ts +16 -10
- package/generated/versions/defold-1.13.0/buffer.d.ts +18 -17
- package/generated/versions/defold-1.13.0/camera.d.ts +27 -27
- package/generated/versions/defold-1.13.0/collectionfactory.d.ts +21 -19
- package/generated/versions/defold-1.13.0/collectionproxy.d.ts +2 -2
- package/generated/versions/defold-1.13.0/compute.d.ts +1 -70
- package/generated/versions/defold-1.13.0/crash.d.ts +2 -2
- package/generated/versions/defold-1.13.0/editor-vm/tilemap_tiles.d.ts +3 -3
- package/generated/versions/defold-1.13.0/editor.d.ts +34 -24
- package/generated/versions/defold-1.13.0/factory.d.ts +15 -16
- package/generated/versions/defold-1.13.0/go.d.ts +15 -17
- package/generated/versions/defold-1.13.0/graphics.d.ts +121 -37
- package/generated/versions/defold-1.13.0/gui.d.ts +111 -193
- package/generated/versions/defold-1.13.0/html5.d.ts +4 -2
- package/generated/versions/defold-1.13.0/http.d.ts +4 -15
- package/generated/versions/defold-1.13.0/image.d.ts +38 -46
- package/generated/versions/defold-1.13.0/index.d.ts +2 -0
- package/generated/versions/defold-1.13.0/json.d.ts +10 -6
- package/generated/versions/defold-1.13.0/label.d.ts +2 -3
- package/generated/versions/defold-1.13.0/liveupdate.d.ts +7 -5
- package/generated/versions/defold-1.13.0/material.d.ts +69 -107
- package/generated/versions/defold-1.13.0/model.d.ts +34 -13
- package/generated/versions/defold-1.13.0/particlefx.d.ts +2 -5
- package/generated/versions/defold-1.13.0/physics.d.ts +82 -78
- package/generated/versions/defold-1.13.0/profiler.d.ts +3 -6
- package/generated/versions/defold-1.13.0/render.d.ts +160 -132
- package/generated/versions/defold-1.13.0/resource.d.ts +217 -210
- package/generated/versions/defold-1.13.0/socket.d.ts +45 -61
- package/generated/versions/defold-1.13.0/sound.d.ts +11 -11
- package/generated/versions/defold-1.13.0/sprite.d.ts +24 -6
- package/generated/versions/defold-1.13.0/sys.d.ts +45 -68
- package/generated/versions/defold-1.13.0/tilemap.d.ts +18 -11
- package/generated/versions/defold-1.13.0/timer.d.ts +5 -3
- package/generated/versions/defold-1.13.0/vmath.d.ts +5 -26
- package/generated/versions/defold-1.13.0/window.d.ts +1 -16
- package/generated/versions/defold-1.13.1/b2d.d.ts +33 -0
- package/generated/versions/defold-1.13.1/b2d_body.d.ts +636 -0
- package/generated/versions/defold-1.13.1/b2d_chain.d.ts +106 -0
- package/generated/versions/defold-1.13.1/b2d_fixture.d.ts +157 -0
- package/generated/versions/defold-1.13.1/b2d_joint.d.ts +823 -0
- package/generated/versions/defold-1.13.1/b2d_shape.d.ts +400 -0
- package/generated/versions/defold-1.13.1/b2d_world.d.ts +320 -0
- package/generated/versions/defold-1.13.1/buffer.d.ts +189 -0
- package/generated/versions/defold-1.13.1/camera.d.ts +303 -0
- package/generated/versions/defold-1.13.1/collectionfactory.d.ts +151 -0
- package/generated/versions/defold-1.13.1/collectionproxy.d.ts +93 -0
- package/generated/versions/defold-1.13.1/compute.d.ts +237 -0
- package/generated/versions/defold-1.13.1/crash.d.ts +143 -0
- package/generated/versions/defold-1.13.1/editor-vm/http.d.ts +56 -0
- package/generated/versions/defold-1.13.1/editor-vm/image.d.ts +48 -0
- package/generated/versions/defold-1.13.1/editor-vm/json.d.ts +10 -0
- package/generated/versions/defold-1.13.1/editor-vm/localization.d.ts +42 -0
- package/generated/versions/defold-1.13.1/editor-vm/tilemap_tiles.d.ts +80 -0
- package/generated/versions/defold-1.13.1/editor-vm/zip.d.ts +10 -0
- package/generated/versions/defold-1.13.1/editor-vm/zlib.d.ts +24 -0
- package/generated/versions/defold-1.13.1/editor.d.ts +1121 -0
- package/generated/versions/defold-1.13.1/factory.d.ts +124 -0
- package/generated/versions/defold-1.13.1/font.d.ts +81 -0
- package/generated/versions/defold-1.13.1/go.d.ts +1107 -0
- package/generated/versions/defold-1.13.1/graphics.d.ts +411 -0
- package/generated/versions/defold-1.13.1/gui.d.ts +2353 -0
- package/generated/versions/defold-1.13.1/html5.d.ts +55 -0
- package/generated/versions/defold-1.13.1/http.d.ts +77 -0
- package/generated/versions/defold-1.13.1/image.d.ts +131 -0
- package/generated/versions/defold-1.13.1/index.d.ts +57 -0
- package/generated/versions/defold-1.13.1/json.d.ts +80 -0
- package/generated/versions/defold-1.13.1/kinds/editor-script.d.ts +14 -0
- package/generated/versions/defold-1.13.1/label.d.ts +93 -0
- package/generated/versions/defold-1.13.1/liveupdate.d.ts +142 -0
- package/generated/versions/defold-1.13.1/material.d.ts +408 -0
- package/generated/versions/defold-1.13.1/model.d.ts +272 -0
- package/generated/versions/defold-1.13.1/msg.d.ts +10 -0
- package/generated/versions/defold-1.13.1/particlefx.d.ts +163 -0
- package/generated/versions/defold-1.13.1/physics.d.ts +559 -0
- package/generated/versions/defold-1.13.1/profiler.d.ts +235 -0
- package/generated/versions/defold-1.13.1/render.d.ts +1190 -0
- package/generated/versions/defold-1.13.1/resource.d.ts +1523 -0
- package/generated/versions/defold-1.13.1/socket.d.ts +784 -0
- package/generated/versions/defold-1.13.1/sound.d.ts +292 -0
- package/generated/versions/defold-1.13.1/sprite.d.ts +175 -0
- package/generated/versions/defold-1.13.1/sys.d.ts +678 -0
- package/generated/versions/defold-1.13.1/tilemap.d.ts +208 -0
- package/generated/versions/defold-1.13.1/timer.d.ts +125 -0
- package/generated/versions/defold-1.13.1/types.d.ts +61 -0
- package/generated/versions/defold-1.13.1/vmath.d.ts +774 -0
- package/generated/versions/defold-1.13.1/window.d.ts +183 -0
- package/generated/versions/defold-1.13.1/zlib.d.ts +41 -0
- package/generated/vmath.d.ts +6 -26
- package/generated/window.d.ts +71 -71
- package/identity-vocabulary.json +561 -0
- package/index.d.ts +8 -0
- package/package.json +13 -2
- package/scripts/example-store-io.ts +12 -1
- package/scripts/extension-goldens.ts +1 -0
- package/scripts/fidelity-baseline.json +146 -46
- package/scripts/generate-api-availability.ts +90 -12
- package/scripts/generate-api-signatures.ts +14 -3
- package/scripts/import-defold-release.ts +85 -24
- package/scripts/prose-constants.ts +157 -0
- package/scripts/regen.ts +396 -78
- package/scripts/shared-handle-methods.ts +53 -0
- package/scripts/signature-store-fs.ts +1 -0
- package/scripts/sync-api-docs.ts +16 -5
- package/src/api-availability.ts +0 -0
- package/src/api-doc.ts +201 -3
- package/src/api-identity.ts +252 -0
- package/src/component-properties.d.ts +33 -0
- package/src/core-types.ts +28 -3
- package/src/correction-pins.ts +49 -0
- package/src/custom-messages.d.ts +4 -3
- package/src/doc-comment.ts +139 -23
- package/src/doc-corrections.ts +108 -0
- package/src/editor-vm-globals.d.ts +5 -2
- package/src/emit-dts.ts +2474 -281
- package/src/emit-messages.ts +52 -0
- package/src/engine-globals.d.ts +2 -2
- package/src/example-store.ts +125 -14
- package/src/field-correction-coverage.ts +204 -0
- package/src/fnv1a.ts +16 -0
- package/src/go-overloads.d.ts +33 -13
- package/src/index-base-table.ts +145 -0
- package/src/index-slot-classifications.ts +588 -0
- package/src/index.ts +39 -2
- package/src/library-index-slot-classifications.ts +332 -0
- package/src/lifecycle.ts +61 -199
- package/src/luals-type-expr.ts +354 -0
- package/src/msg-overloads.d.ts +0 -1
- package/src/param-correction-provenance.ts +64 -0
- package/src/return-correction-provenance.ts +58 -16
- package/src/script-api.ts +3 -1
- package/src/sys-overloads.d.ts +48 -0
- package/src/vmath-overloads.d.ts +27 -1
- package/src/window-event-guard.d.ts +6 -9
- package/url-parameters.json +1 -2
package/src/emit-dts.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import messagesDoc from "../fixtures/messages_doc.json" with { type: "json" };
|
|
1
2
|
import { type ApiSymbolIdentity, normalizedFunctionSignature } from "./api-availability";
|
|
2
3
|
import type {
|
|
3
4
|
ApiConstant,
|
|
@@ -5,22 +6,41 @@ import type {
|
|
|
5
6
|
ApiModule,
|
|
6
7
|
ApiParameter,
|
|
7
8
|
ApiProperty,
|
|
9
|
+
ApiStruct,
|
|
10
|
+
ApiStructMember,
|
|
11
|
+
ApiTypedef,
|
|
8
12
|
ApiVariable,
|
|
9
13
|
} from "./api-doc";
|
|
10
14
|
import { DEFOLD_TYPE_MAP } from "./core-types";
|
|
15
|
+
import { correctionPins, pinMatches, propertyCorrectionPins } from "./correction-pins";
|
|
11
16
|
import {
|
|
12
17
|
type DocCommentParts,
|
|
13
18
|
htmlToCodeText,
|
|
14
19
|
htmlToDocText,
|
|
15
20
|
renderDocComment,
|
|
16
21
|
splitExampleSources,
|
|
22
|
+
summaryFor,
|
|
17
23
|
} from "./doc-comment";
|
|
18
24
|
import type { TranslationStore } from "./example-store";
|
|
19
|
-
import {
|
|
25
|
+
import { lookupExampleSegments, translateProseFences } from "./example-store";
|
|
26
|
+
import {
|
|
27
|
+
appendNotes,
|
|
28
|
+
type IndexBaseNoteResolver,
|
|
29
|
+
type IndexBaseSource,
|
|
30
|
+
indexBaseNoteResolver,
|
|
31
|
+
OVERRIDE_RETURN_SLOT,
|
|
32
|
+
} from "./index-slot-classifications";
|
|
33
|
+
import { type LualsPosition, mapLualsExpression, needsArrayParens } from "./luals-type-expr";
|
|
20
34
|
import { classifyUrlParameter, type UrlParameterTable } from "./url-parameters";
|
|
21
35
|
|
|
36
|
+
export { summaryFor };
|
|
37
|
+
|
|
38
|
+
// Maps one ref-doc type token. `position` says whether the caller supplies the
|
|
39
|
+
// value (a parameter) or the engine hands it back; see `LualsPosition`.
|
|
40
|
+
export type DefoldTypeMapper = (token: string, position?: LualsPosition) => string;
|
|
41
|
+
|
|
22
42
|
export interface EmitOptions {
|
|
23
|
-
mapType?:
|
|
43
|
+
mapType?: DefoldTypeMapper;
|
|
24
44
|
// Constant FQNs from *other* modules, so a foreign token like
|
|
25
45
|
// `graphics.BUFFER_TYPE_COLOR0_BIT` used as a param type inside `render`
|
|
26
46
|
// brands to the same FQN-keyed type its owning module's `const` emits,
|
|
@@ -38,6 +58,23 @@ export interface EmitOptions {
|
|
|
38
58
|
// emit. `regen` supplies the committed `url-parameters.json`; the table
|
|
39
59
|
// arrives as data so this module stays free of `node:fs` (bug-88).
|
|
40
60
|
urlParameters?: UrlParameterTable;
|
|
61
|
+
// Which classification map the index-base notes read. Defaults to the engine
|
|
62
|
+
// map; a library or extension declaration passes its page keys (`[]` for one
|
|
63
|
+
// no page documents), so it never names Defold as the receiver.
|
|
64
|
+
indexBaseSource?: IndexBaseSource;
|
|
65
|
+
// The TS path of each `ENUM`, `STRUCT` and `TYPEDEF` another module declares
|
|
66
|
+
// (`declaredTypePaths`), so a token naming one resolves instead of widening to
|
|
67
|
+
// `unknown`. The module's own declarations resolve without it. Read only by the
|
|
68
|
+
// default engine mapper; a caller-supplied `mapType` keeps its own vocabulary.
|
|
69
|
+
declaredTypes?: ReadonlyMap<string, string>;
|
|
70
|
+
// Tokens a surface names that the engine vocabulary does not (the editor's
|
|
71
|
+
// handles), resolved ahead of it both as whole tokens and as leaves inside a
|
|
72
|
+
// composite. Read by the default mapper together with the declared types.
|
|
73
|
+
typeLeaves?: Readonly<Record<string, string>>;
|
|
74
|
+
// Constant FQNs the target's own engine bindings register, which gate the
|
|
75
|
+
// `UNDOCUMENTED_CONSTANTS` back-fill. Absent for a surface with no vendored
|
|
76
|
+
// bindings (a ref-doc target), where every entry the ref-doc omits is emitted.
|
|
77
|
+
registeredConstants?: ReadonlySet<string>;
|
|
41
78
|
}
|
|
42
79
|
|
|
43
80
|
export const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
@@ -131,6 +168,8 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
|
|
|
131
168
|
// names; bare `on_message`'s receive `message` is the pre-hashed, user-routed
|
|
132
169
|
// payload (mitigated for authored code by isMessage/onMessage). No static shape
|
|
133
170
|
// exists, so `Record<string | number, unknown>` is faithful, not a loss.
|
|
171
|
+
// collectionfactory.create curates the instance-id layer of `properties` as a
|
|
172
|
+
// mapping; each per-instance property table stays that faithful `Record`.
|
|
134
173
|
"factory.create",
|
|
135
174
|
"collectionfactory.create",
|
|
136
175
|
"on_message",
|
|
@@ -172,11 +211,6 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
|
|
|
172
211
|
// not a loss. The audit skips counting these; the emitter already leaves an
|
|
173
212
|
// uncurated slot as `Record`. Add deliberately, one rationale per line.
|
|
174
213
|
export const ARBITRARY_TABLE_SLOT_KEYS = new Set([
|
|
175
|
-
// "the chain definition" / "the shape definition" / "explosion definition" —
|
|
176
|
-
// create-side option tables with no documented field list at this slot.
|
|
177
|
-
"b2d.body.create_chain:param:definition",
|
|
178
|
-
"b2d.body.create_shape:param:definition",
|
|
179
|
-
"b2d.world.explode:param:definition",
|
|
180
214
|
"b2d.joint.create_filter:param:definition", // "optional definition table"
|
|
181
215
|
// opaque Box2D result structs: the doc names no fields.
|
|
182
216
|
"b2d.chain.get_geometry:return:geometry", // "chain geometry table"
|
|
@@ -194,6 +228,18 @@ export const ARBITRARY_TABLE_SLOT_KEYS = new Set([
|
|
|
194
228
|
// set to keep the `droppedMembers` count honest; an uncovered skip (e.g. a new
|
|
195
229
|
// `skipFunctions` entry with no matching overload) still counts, so the gate
|
|
196
230
|
// stays a real signal. Mirrors the ARBITRARY_TABLE_SLOTS audit-honesty pattern.
|
|
231
|
+
// A slot the engine treats as omissible on a function OVERLOAD_COVERED_SKIPS
|
|
232
|
+
// hands to a `*-overloads.d.ts` form without it, where no generated declaration
|
|
233
|
+
// exists for OPTIONAL_SLOT_CORRECTIONS to reach. Availability identity reads it
|
|
234
|
+
// as optional, so a ref-doc that starts marking it is no change. The value is
|
|
235
|
+
// the evidence, as in OPTIONAL_SLOT_CORRECTIONS.
|
|
236
|
+
export const HAND_DECLARED_OPTIONAL_SLOTS: ReadonlyMap<string, string> = new Map(
|
|
237
|
+
(["y", "z"] as const).map((slot) => [
|
|
238
|
+
`vmath.euler_to_quat:param:${slot}`,
|
|
239
|
+
"example `vmath.euler_to_quat(v)` passes a vector3 alone; vmath-overloads.d.ts declares that form",
|
|
240
|
+
]),
|
|
241
|
+
);
|
|
242
|
+
|
|
197
243
|
export const OVERLOAD_COVERED_SKIPS = new Set([
|
|
198
244
|
// go-overloads.d.ts supplies the typed go.get / go.set / go.property shapes.
|
|
199
245
|
"go.get",
|
|
@@ -208,14 +254,55 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
|
|
|
208
254
|
"render.render_target",
|
|
209
255
|
// vmath-overloads.d.ts supplies the vector3 and three-number euler_to_quat arities.
|
|
210
256
|
"vmath.euler_to_quat",
|
|
257
|
+
// vmath-overloads.d.ts supplies the empty form and the table form; the engine
|
|
258
|
+
// raises on an explicit nil, so no optional parameter admits undefined.
|
|
259
|
+
"vmath.vector",
|
|
211
260
|
// vmath-overloads.d.ts supplies the generic covariant signatures.
|
|
212
261
|
"vmath.clamp",
|
|
213
262
|
"vmath.lerp",
|
|
214
263
|
"vmath.slerp",
|
|
215
264
|
"vmath.mul_per_elem",
|
|
216
265
|
"vmath.normalize",
|
|
266
|
+
// sys-overloads.d.ts supplies the disabling form and the form that takes the
|
|
267
|
+
// cooldown the engine requires when enabling.
|
|
268
|
+
"sys.set_engine_throttle",
|
|
269
|
+
// sys-overloads.d.ts supplies the form without a default, which returns nil
|
|
270
|
+
// for a missing key, and the form with one, which always returns a string.
|
|
271
|
+
"sys.get_config_string",
|
|
217
272
|
]);
|
|
218
273
|
|
|
274
|
+
// A declaration upstream files under another function's name: its return slot
|
|
275
|
+
// and prose are the named sibling's, so the element keeps only its own
|
|
276
|
+
// declarations. Keyed `<fqn>:<first return name>`, which tells the misfiled
|
|
277
|
+
// declaration from the real one. `return-correction-provenance.test.ts` reds
|
|
278
|
+
// once no retained target still files it, and the entry is then deleted.
|
|
279
|
+
export const MISFILED_DECLARATIONS: ReadonlyMap<string, string> = new Map([
|
|
280
|
+
// b2d_body_doc.json repeats `get_angle`'s `angle` return under `get_world_center`.
|
|
281
|
+
["b2d.body.get_world_center:angle", "b2d.body.get_angle"],
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
function isMisfiled(fn: ApiFunction): boolean {
|
|
285
|
+
return MISFILED_DECLARATIONS.has(`${fn.name}:${fn.returnValues[0]?.name ?? ""}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// skipFunction FQNs withheld because no engine binding registers them in the
|
|
289
|
+
// target that documents them, so the declared call would raise. Like
|
|
290
|
+
// OVERLOAD_COVERED_SKIPS, the audit leaves them out of `droppedMembers`: the
|
|
291
|
+
// surface loses nothing a script could call. The engine-binding diff reports a
|
|
292
|
+
// missing binding if one is declared again.
|
|
293
|
+
export const UNBOUND_SKIPS = new Set([
|
|
294
|
+
"b2d.body.get_contact_list",
|
|
295
|
+
"b2d.body.get_user_data",
|
|
296
|
+
"b2d.body.set_user_data",
|
|
297
|
+
"b2d.body.reset_mass_data",
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
// skipFunction FQNs withheld because another lane's declaration takes the same
|
|
301
|
+
// global name: the runtime `http.response` table beside the editor VM's
|
|
302
|
+
// `http.response` handle. The audit leaves them out of `droppedMembers`, since
|
|
303
|
+
// a slot that needs the shape types it inline.
|
|
304
|
+
export const NAME_CLASH_SKIPS = new Set(["http.response"]);
|
|
305
|
+
|
|
219
306
|
// Element names whose `table` slot is a prose-only `a table mapping X to Y`
|
|
220
307
|
// shape the field-list parser cannot read, but whose key/value a human curated
|
|
221
308
|
// from the doc. Emitted as `LuaMap<K, V>` because the key is a branded `Hash`,
|
|
@@ -244,6 +331,17 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
|
|
|
244
331
|
// `returnvalues` is empty while every sibling `editor.tx.*` builder declares
|
|
245
332
|
// one, and `editor.transact()` takes exactly that.
|
|
246
333
|
["editor.tx.add", 'Opaque<"transaction_step">'],
|
|
334
|
+
// The engine binding pushes a value the ref-doc never documents:
|
|
335
|
+
// script_sys.cpp:Sys_Save pushes whether the write succeeded,
|
|
336
|
+
// script_tilemap.cpp:TileMap_SetTile pushes false for an unknown layer or an
|
|
337
|
+
// out-of-range cell, and script_box2d_body_v3.cpp:Body_CreateShape pushes the
|
|
338
|
+
// created shape's info table through PushShapeInfo.
|
|
339
|
+
["sys.save", "boolean"],
|
|
340
|
+
["tilemap.set_tile", "boolean"],
|
|
341
|
+
[
|
|
342
|
+
"b2d.body.create_shape",
|
|
343
|
+
'{ index: number; shape_id: Opaque<"b2Shape">; type: number; sensor: boolean; density: number; friction: number; restitution: number; material: number; child_count: number; is_chain_segment: boolean }',
|
|
344
|
+
],
|
|
247
345
|
]);
|
|
248
346
|
|
|
249
347
|
// A function whose `returnvalues` declare a concrete type while the same slot's
|
|
@@ -254,10 +352,25 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
|
|
|
254
352
|
// ref-doc. Upstream adding `nil` to `types` hands the case to the top-level
|
|
255
353
|
// `nil` projection in `mapSlotUnion` and reds the entry, which is then deleted
|
|
256
354
|
// rather than re-pinned.
|
|
355
|
+
//
|
|
356
|
+
// The same table holds a `function` return whose prose documents the returned
|
|
357
|
+
// function's contract, which the bare token cannot carry: `socket.protect`'s
|
|
358
|
+
// safe function "returns nil followed by an error message", and
|
|
359
|
+
// `socket.newtry`'s try function returns its first argument when it is truthy
|
|
360
|
+
// and throws otherwise, the LuaSocket contract its example relies on to unwrap
|
|
361
|
+
// `socket.connect`'s `client, err` pair. The try function takes that pair
|
|
362
|
+
// destructured into separate arguments; a boxed `LuaMultiReturn` is rejected
|
|
363
|
+
// because TSTL passes it as one table, which is always truthy. `reason` quotes
|
|
364
|
+
// the slot prose the provenance test searches for, as for the `nil` entries.
|
|
257
365
|
export interface ReturnTypeCorrection {
|
|
258
366
|
readonly ts: string;
|
|
259
367
|
readonly upstream: readonly string[];
|
|
368
|
+
// A later release's spelling of the same defect; see `correction-pins.ts`.
|
|
369
|
+
readonly retypedUpstream?: readonly (readonly string[])[];
|
|
260
370
|
readonly reason: string;
|
|
371
|
+
// The named value of a multi-return the correction rewrites; `ts` is then
|
|
372
|
+
// that value's type. Absent, the correction rewrites a single return.
|
|
373
|
+
readonly slot?: string;
|
|
261
374
|
}
|
|
262
375
|
|
|
263
376
|
// Keyed by FQN, mirroring PROPERTY_TYPE_CORRECTIONS.
|
|
@@ -270,6 +383,374 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
|
|
|
270
383
|
reason: "the body if successful. Otherwise nil.",
|
|
271
384
|
},
|
|
272
385
|
],
|
|
386
|
+
[
|
|
387
|
+
"b2d.shape.ray_cast",
|
|
388
|
+
{
|
|
389
|
+
ts: "{ point: Vector3; normal: Vector3; fraction: number; iterations: number } | undefined",
|
|
390
|
+
upstream: ["table"],
|
|
391
|
+
reason: "and iterations, or nil",
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
[
|
|
395
|
+
"b2d.world.cast_ray_closest",
|
|
396
|
+
{
|
|
397
|
+
ts: "{ fixture: number; shape: number; point: Vector3; normal: Vector3; fraction: number; node_visits: number; leaf_visits: number } | undefined",
|
|
398
|
+
upstream: ["table"],
|
|
399
|
+
reason: "and leaf_visits, or nil",
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
[
|
|
403
|
+
"gui.new_texture",
|
|
404
|
+
{
|
|
405
|
+
ts: "number | undefined",
|
|
406
|
+
upstream: ["number"],
|
|
407
|
+
reason: "codes if unsuccessful",
|
|
408
|
+
slot: "code",
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
// 1.13.2 declares `render_predicate`, whose TYPEDEF aliases `userdata`, and
|
|
412
|
+
// that still emits `number`.
|
|
413
|
+
[
|
|
414
|
+
"render.predicate",
|
|
415
|
+
{
|
|
416
|
+
ts: 'Opaque<"render_predicate">',
|
|
417
|
+
upstream: ["number"],
|
|
418
|
+
retypedUpstream: [["render_predicate"]],
|
|
419
|
+
reason: "new predicate",
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
// 1.13.2 declares both as `fun(...:any):any`, which emits
|
|
423
|
+
// `(...args: unknown[]) => unknown` and still drops the contract.
|
|
424
|
+
[
|
|
425
|
+
"socket.protect",
|
|
426
|
+
{
|
|
427
|
+
ts: "(...args: unknown[]) => LuaMultiReturn<[unknown, string | undefined]>",
|
|
428
|
+
upstream: ["function(function())"],
|
|
429
|
+
retypedUpstream: [["fun(...:any):any"]],
|
|
430
|
+
reason: "returns nil followed by an error message.",
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
[
|
|
434
|
+
"socket.newtry",
|
|
435
|
+
{
|
|
436
|
+
ts: "<T>(value: T | undefined, ...rest: T extends { readonly __tstlMultiReturn: unknown } ? [boxedMultiReturn: never] : unknown[]) => T",
|
|
437
|
+
upstream: ["function"],
|
|
438
|
+
retypedUpstream: [["fun(...:any):any"]],
|
|
439
|
+
reason: "the customized try function.",
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
// A parameter whose declared `types` disagree with what the same element's
|
|
445
|
+
// prose, an upstream example, or the engine binding demonstrably takes — a render
|
|
446
|
+
// target by resource name, a texture unit, a Lua sequence where `table` is
|
|
447
|
+
// declared, a hash the binding refuses as a string. Admission rule: only a slot
|
|
448
|
+
// that evidence contradicts; a slot that is merely imprecise is not a candidate.
|
|
449
|
+
// The correction edits the mapped union rather than replacing it: `adds` appends
|
|
450
|
+
// what the slot also takes and `removes` drops members the binding raises on, so
|
|
451
|
+
// the URL address alias, array curation and constant mapping stay live, and
|
|
452
|
+
// optionality stays with `isDocOptional`. Each entry records the upstream tokens
|
|
453
|
+
// it contradicts, plus any later spelling of the same defect in
|
|
454
|
+
// `retypedUpstream`, and applies only to a slot still declaring one of them;
|
|
455
|
+
// `param-correction-provenance.test.ts` pins them against every retained
|
|
456
|
+
// vendored ref-doc. Once no retained target still declares any of them the
|
|
457
|
+
// entry is deleted, never re-pinned.
|
|
458
|
+
export interface ParamTypeCorrection {
|
|
459
|
+
readonly adds?: string;
|
|
460
|
+
readonly removes?: readonly string[];
|
|
461
|
+
readonly upstream: readonly string[];
|
|
462
|
+
readonly retypedUpstream?: readonly (readonly string[])[];
|
|
463
|
+
readonly reason: string;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const CAMERA_ADDRESS_REASON =
|
|
467
|
+
"render_script_camera.cpp:CheckRenderCamera resolves anything but a number handle through ResolveURL, which takes a string or a hash";
|
|
468
|
+
|
|
469
|
+
const CAMERA_SLOT_FUNCTIONS = [
|
|
470
|
+
"get_aspect_ratio",
|
|
471
|
+
"get_auto_aspect_ratio",
|
|
472
|
+
"get_enabled",
|
|
473
|
+
"get_far_z",
|
|
474
|
+
"get_fov",
|
|
475
|
+
"get_near_z",
|
|
476
|
+
"get_orthographic_auto_zoom",
|
|
477
|
+
"get_orthographic_mode",
|
|
478
|
+
"get_orthographic_zoom",
|
|
479
|
+
"get_projection",
|
|
480
|
+
"get_view",
|
|
481
|
+
"screen_to_world",
|
|
482
|
+
"screen_xy_to_world",
|
|
483
|
+
"set_aspect_ratio",
|
|
484
|
+
"set_auto_aspect_ratio",
|
|
485
|
+
"set_far_z",
|
|
486
|
+
"set_fov",
|
|
487
|
+
"set_near_z",
|
|
488
|
+
"set_orthographic_mode",
|
|
489
|
+
"set_orthographic_zoom",
|
|
490
|
+
"world_to_screen",
|
|
491
|
+
] as const;
|
|
492
|
+
|
|
493
|
+
// Keyed `<element>:param:<slot>` like OPTIONAL_SLOT_CORRECTIONS, so a
|
|
494
|
+
// same-named slot on another element is untouched.
|
|
495
|
+
export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> = new Map<
|
|
496
|
+
string,
|
|
497
|
+
ParamTypeCorrection
|
|
498
|
+
>([
|
|
499
|
+
...CAMERA_SLOT_FUNCTIONS.map(
|
|
500
|
+
(name) =>
|
|
501
|
+
[
|
|
502
|
+
`camera.${name}:param:camera`,
|
|
503
|
+
{
|
|
504
|
+
adds: "string | Hash",
|
|
505
|
+
upstream: ["url", "number", "nil"],
|
|
506
|
+
reason: CAMERA_ADDRESS_REASON,
|
|
507
|
+
},
|
|
508
|
+
] as const,
|
|
509
|
+
),
|
|
510
|
+
...(["set_group", "get_maskbit", "set_maskbit"] as const).map(
|
|
511
|
+
(name) =>
|
|
512
|
+
[
|
|
513
|
+
`physics.${name}:param:group`,
|
|
514
|
+
{
|
|
515
|
+
adds: "Hash",
|
|
516
|
+
upstream: ["string"],
|
|
517
|
+
reason: `script_physics.cpp reads the ${name} group with CheckHashOrString(L, 2)`,
|
|
518
|
+
},
|
|
519
|
+
] as const,
|
|
520
|
+
),
|
|
521
|
+
[
|
|
522
|
+
"resource.get_text_metrics:param:url",
|
|
523
|
+
{
|
|
524
|
+
adds: "string",
|
|
525
|
+
upstream: ["hash"],
|
|
526
|
+
reason: "script_resource.cpp:GetTextMetrics reads the font path with CheckHashOrString(L, 1)",
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
[
|
|
530
|
+
"collectionproxy.get_resources:param:collectionproxy",
|
|
531
|
+
{
|
|
532
|
+
adds: "string | Hash",
|
|
533
|
+
upstream: ["url"],
|
|
534
|
+
reason:
|
|
535
|
+
"script_collectionproxy.cpp:CollectionProxy_GetResourcesInternal resolves the proxy through GetComponentFromLua, which takes a string or a hash",
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
...(["screen_to_local", "set_screen_position"] as const).map(
|
|
539
|
+
(name) =>
|
|
540
|
+
[
|
|
541
|
+
`gui.${name}:param:screen_position`,
|
|
542
|
+
{
|
|
543
|
+
adds: "Vector4",
|
|
544
|
+
upstream: ["vector3"],
|
|
545
|
+
reason: "gui_script.cpp:GetPositionFromArgumentIndex takes a vector3 or a vector4",
|
|
546
|
+
},
|
|
547
|
+
] as const,
|
|
548
|
+
),
|
|
549
|
+
[
|
|
550
|
+
"particlefx.set_constant:param:value",
|
|
551
|
+
{
|
|
552
|
+
adds: "Matrix4",
|
|
553
|
+
upstream: ["vector4"],
|
|
554
|
+
reason: "script_particlefx.cpp:ParticleFX_SetConstant takes a vector4 or a matrix4 value",
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
...(["animate", "cancel_animations"] as const).map(
|
|
558
|
+
(name) =>
|
|
559
|
+
[
|
|
560
|
+
`gui.${name}:param:property`,
|
|
561
|
+
{
|
|
562
|
+
adds: "Hash",
|
|
563
|
+
upstream: name === "animate" ? ["string", "constant"] : ["nil", "string", "constant"],
|
|
564
|
+
reason: `gui_script.cpp reads the ${name} property with CheckHashOrString(L, 2)`,
|
|
565
|
+
},
|
|
566
|
+
] as const,
|
|
567
|
+
),
|
|
568
|
+
[
|
|
569
|
+
"sound.get_group_name:param:group",
|
|
570
|
+
{
|
|
571
|
+
removes: ["string"],
|
|
572
|
+
upstream: ["string", "hash"],
|
|
573
|
+
reason: "script_sound.cpp:Sound_GetGroupName reads CheckHash(L, 1), which refuses a string",
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
...(["get_mesh_enabled", "set_mesh_enabled"] as const).map(
|
|
577
|
+
(name) =>
|
|
578
|
+
[
|
|
579
|
+
`model.${name}:param:mesh_id`,
|
|
580
|
+
{
|
|
581
|
+
removes: ["Url"],
|
|
582
|
+
upstream: ["string", "hash", "url"],
|
|
583
|
+
reason: `script_model.cpp reads the ${name} mesh id with CheckHashOrString(L, 2), which refuses a url`,
|
|
584
|
+
},
|
|
585
|
+
] as const,
|
|
586
|
+
),
|
|
587
|
+
[
|
|
588
|
+
"resource.create_texture_async:param:path",
|
|
589
|
+
{
|
|
590
|
+
removes: ["Hash"],
|
|
591
|
+
upstream: ["string", "hash"],
|
|
592
|
+
reason:
|
|
593
|
+
"script_resource.cpp:CheckCreateTextureResourceParams reads the path with luaL_checkstring(L, 1), which refuses a hash",
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
[
|
|
597
|
+
"render.set_render_target:param:render_target",
|
|
598
|
+
{
|
|
599
|
+
adds: "string | Hash | typeof render.RENDER_TARGET_DEFAULT",
|
|
600
|
+
upstream: ["render_target"],
|
|
601
|
+
retypedUpstream: [["render_target", "string", "hash", "nil"]],
|
|
602
|
+
reason:
|
|
603
|
+
"\"render.RENDER_TARGET_DEFAULT to set the default render target\"; example `render.set_render_target('my_rt_resource')`; 1.13.2 adds the names but still omits the constant",
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
[
|
|
607
|
+
"render.get_render_target_width:param:render_target",
|
|
608
|
+
{
|
|
609
|
+
adds: "string | Hash",
|
|
610
|
+
upstream: ["render_target"],
|
|
611
|
+
reason:
|
|
612
|
+
"example `render.get_render_target_width('my_rt_resource', graphics.BUFFER_TYPE_COLOR0_BIT)`",
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
[
|
|
616
|
+
"render.get_render_target_height:param:render_target",
|
|
617
|
+
{
|
|
618
|
+
adds: "string | Hash",
|
|
619
|
+
upstream: ["render_target"],
|
|
620
|
+
reason:
|
|
621
|
+
"example `render.get_render_target_height('my_rt_resource', graphics.BUFFER_TYPE_COLOR0_BIT)`",
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
[
|
|
625
|
+
"render.set_render_target_size:param:render_target",
|
|
626
|
+
{
|
|
627
|
+
adds: "string | Hash",
|
|
628
|
+
upstream: ["render_target"],
|
|
629
|
+
reason:
|
|
630
|
+
"\"a render target created from either a render script, or from a render target resource\"; example `render.set_render_target_size('my_rt_resource', ...)`",
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
[
|
|
634
|
+
"render.enable_texture:param:handle_or_name",
|
|
635
|
+
{
|
|
636
|
+
adds: 'Opaque<"render_target"> | number',
|
|
637
|
+
upstream: ["texture", "string", "hash"],
|
|
638
|
+
reason:
|
|
639
|
+
"\"render target or texture handle that should be bound, or a named resource\"; examples `render.enable_texture(0, self.my_render_target, ...)` and `render.enable_texture(0, 'my_rt_resource', ...)`; `resource.get_texture_info` returns the handle as a number",
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
[
|
|
643
|
+
"render.disable_texture:param:binding",
|
|
644
|
+
{
|
|
645
|
+
adds: "number",
|
|
646
|
+
upstream: ["texture", "string", "hash"],
|
|
647
|
+
reason:
|
|
648
|
+
'"texture binding, either by texture unit, string or hash"; example `render.disable_texture(0)`',
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
[
|
|
652
|
+
"render.set_camera:param:camera",
|
|
653
|
+
{
|
|
654
|
+
adds: "string | Hash",
|
|
655
|
+
upstream: ["url", "number", "nil"],
|
|
656
|
+
reason:
|
|
657
|
+
'example `render.set_camera("main:/my_go#camera")`; render_script.cpp:RenderScript_SetCamera resolves a non-number camera through ResolveURL, which also takes a hash',
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
[
|
|
661
|
+
"render.draw:param:predicate",
|
|
662
|
+
{
|
|
663
|
+
adds: 'Opaque<"render_predicate">',
|
|
664
|
+
removes: ["number"],
|
|
665
|
+
upstream: ["number"],
|
|
666
|
+
retypedUpstream: [["render_predicate"]],
|
|
667
|
+
reason:
|
|
668
|
+
"render_script.cpp:RenderScript_Draw reads the predicate with RenderScriptPredicate_Check, the userdata render.predicate returns; 1.13.2's `render_predicate` still emits `number`",
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
[
|
|
672
|
+
"go.delete:param:id",
|
|
673
|
+
{
|
|
674
|
+
adds: "boolean",
|
|
675
|
+
upstream: ["string", "hash", "url", "table"],
|
|
676
|
+
retypedUpstream: [["string", "hash", "url", "(string|hash|url)[]"]],
|
|
677
|
+
reason:
|
|
678
|
+
"example `go.delete(true)` deletes the script game object and its children; 1.13.2 types the id list but still omits `boolean`",
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
[
|
|
682
|
+
"buffer.get_bytes:param:stream_name",
|
|
683
|
+
{
|
|
684
|
+
adds: "string",
|
|
685
|
+
upstream: ["hash"],
|
|
686
|
+
reason: 'sys.load_buffer example `buffer.get_bytes(my_buffer, "data")`',
|
|
687
|
+
},
|
|
688
|
+
],
|
|
689
|
+
[
|
|
690
|
+
"gui.set:param:value",
|
|
691
|
+
{
|
|
692
|
+
adds: "Matrix4 | Hash",
|
|
693
|
+
upstream: ["number", "vector4", "vector3", "quaternion"],
|
|
694
|
+
reason:
|
|
695
|
+
'example `gui.set(node, "light_matrix", vmath.matrix4())` ("matrix4 is also supported"); example `gui.set(msg.url(), "textures", atlas_id, {key = "runtime_texture"})` passes the hash `resource.create_atlas` returns',
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
[
|
|
699
|
+
"gui.set_rotation:param:rotation",
|
|
700
|
+
{
|
|
701
|
+
adds: "Vector3",
|
|
702
|
+
upstream: ["quaternion", "vector4"],
|
|
703
|
+
reason: "gui.set example `gui.set_rotation(node, vmath.vector3(0,0,45))`",
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
[
|
|
707
|
+
"sys.save:param:table",
|
|
708
|
+
{
|
|
709
|
+
adds: "readonly unknown[]",
|
|
710
|
+
upstream: ["table"],
|
|
711
|
+
retypedUpstream: [["table<any, any>"]],
|
|
712
|
+
reason:
|
|
713
|
+
'example builds `my_table` with `table.insert(my_table, "my_value")` and passes it to `sys.save`; 1.13.2\'s `table<any, any>` emits a `LuaTable`, which refuses an array',
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
[
|
|
717
|
+
"sys.serialize:param:table",
|
|
718
|
+
{
|
|
719
|
+
adds: "readonly unknown[]",
|
|
720
|
+
upstream: ["table"],
|
|
721
|
+
retypedUpstream: [["table<any, any>"]],
|
|
722
|
+
reason:
|
|
723
|
+
'example builds `my_table` with `table.insert(my_table, "my_value")` and passes it to `sys.serialize`; 1.13.2\'s `table<any, any>` emits a `LuaTable`, which refuses an array',
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
[
|
|
727
|
+
"window.set_dim_mode:param:mode",
|
|
728
|
+
{
|
|
729
|
+
removes: ["window.DIMMING"],
|
|
730
|
+
adds: "window.DimModeStateSettable",
|
|
731
|
+
upstream: ["window.DIMMING"],
|
|
732
|
+
reason:
|
|
733
|
+
'script_window.cpp:SetDimMode raises "The dim mode specified is not supported." for anything but DIMMING_ON and DIMMING_OFF',
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
...(["texture", "render_target"] as const).map((handle): [string, ParamTypeCorrection] => [
|
|
737
|
+
`resource.get_${handle}_info:param:path`,
|
|
738
|
+
{
|
|
739
|
+
adds: "number",
|
|
740
|
+
upstream: ["hash", "string", handle],
|
|
741
|
+
reason:
|
|
742
|
+
"script_resource.cpp reads any number in slot 1 as the handle, which `info.handle` returns and older targets declared as `number`",
|
|
743
|
+
},
|
|
744
|
+
]),
|
|
745
|
+
[
|
|
746
|
+
"tilemap.set_tile:param:transform_bitmask",
|
|
747
|
+
{
|
|
748
|
+
adds: "number",
|
|
749
|
+
upstream: ["tilemap.TRANSFORM"],
|
|
750
|
+
reason:
|
|
751
|
+
"a bitmask: the ref-doc example passes tilemap.H_FLIP + tilemap.V_FLIP + tilemap.ROTATE_90, a sum no single TRANSFORM member names",
|
|
752
|
+
},
|
|
753
|
+
],
|
|
273
754
|
]);
|
|
274
755
|
|
|
275
756
|
// A property whose upstream `<span class="type">` states a type the engine does
|
|
@@ -285,6 +766,8 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
|
|
|
285
766
|
export interface PropertyTypeCorrection {
|
|
286
767
|
readonly ts: string;
|
|
287
768
|
readonly upstream: string;
|
|
769
|
+
// A later release's span for the same defect; see `correction-pins.ts`.
|
|
770
|
+
readonly retypedUpstream?: readonly string[];
|
|
288
771
|
readonly reason: string;
|
|
289
772
|
}
|
|
290
773
|
|
|
@@ -300,6 +783,24 @@ export const PROPERTY_TYPE_CORRECTIONS: ReadonlyMap<string, PropertyTypeCorrecti
|
|
|
300
783
|
"a count of animation frames, which the prose confirms; `hash` is a copy-paste of the sibling animation-id spans",
|
|
301
784
|
},
|
|
302
785
|
],
|
|
786
|
+
[
|
|
787
|
+
"go.scale",
|
|
788
|
+
{
|
|
789
|
+
ts: "number | Vector3",
|
|
790
|
+
upstream: "number",
|
|
791
|
+
reason:
|
|
792
|
+
"the engine reads a vector3 back from `go.get`, and `go.set` also takes a number, which it applies as a uniform scale",
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
[
|
|
796
|
+
"label.scale",
|
|
797
|
+
{
|
|
798
|
+
ts: "Vector3",
|
|
799
|
+
upstream: "number | vector3",
|
|
800
|
+
reason:
|
|
801
|
+
'the engine reads a vector3 back from `go.get`, and `go.set` raises "must be a vmath.vector3" when given a number',
|
|
802
|
+
},
|
|
803
|
+
],
|
|
303
804
|
[
|
|
304
805
|
"camera.projection",
|
|
305
806
|
{
|
|
@@ -320,15 +821,36 @@ export const PROPERTY_TYPE_CORRECTIONS: ReadonlyMap<string, PropertyTypeCorrecti
|
|
|
320
821
|
],
|
|
321
822
|
]);
|
|
322
823
|
|
|
824
|
+
// A property upstream names with a placeholder standing for a numbered family of
|
|
825
|
+
// keys, so its literal name is not a key the engine accepts. The emitted members
|
|
826
|
+
// are one literal key per number in `first..last`, each typed like the
|
|
827
|
+
// placeholder. `evidence` is the brief phrase that defines the range;
|
|
828
|
+
// `property-correction-provenance.test.ts` reds when a vendored ref-doc stops
|
|
829
|
+
// saying it, or when the phrase stops stating `first-last`. Curated rather than
|
|
830
|
+
// matched on a trailing `N`, so each new upstream placeholder is a deliberate
|
|
831
|
+
// addition.
|
|
832
|
+
export interface PropertyKeyRange {
|
|
833
|
+
readonly prefix: string;
|
|
834
|
+
readonly first: number;
|
|
835
|
+
readonly last: number;
|
|
836
|
+
readonly evidence: string;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Keyed `<namespace>.<property>`, mirroring PROPERTY_TYPE_CORRECTIONS.
|
|
840
|
+
export const PROPERTY_KEY_RANGES: ReadonlyMap<string, PropertyKeyRange> = new Map([
|
|
841
|
+
["model.textureN", { prefix: "texture", first: 0, last: 7, evidence: "textureN where N is 0-7" }],
|
|
842
|
+
]);
|
|
843
|
+
|
|
323
844
|
// A parameter the ref-doc metadata marks required while the same element's prose
|
|
324
845
|
// or examples show it being omitted. `is_optional` is the only optionality
|
|
325
846
|
// signal the emitter has, and upstream leaves it `False` on slots its own
|
|
326
847
|
// examples skip — so the emitted declaration rejects the call the docs teach.
|
|
327
848
|
//
|
|
328
849
|
// Keyed `<element>:param:<slot>`, the shape `tableSlotKey` builds, so a
|
|
329
|
-
// same-named slot on another element is untouched. The value is the
|
|
330
|
-
//
|
|
331
|
-
//
|
|
850
|
+
// same-named slot on another element is untouched. The value is the evidence for
|
|
851
|
+
// that one entry: the example call that omits it, the doc phrase that names it
|
|
852
|
+
// optional, or the engine binding that reads it as omissible
|
|
853
|
+
// (`<file>:<cFunction>`). Both emitted shapes follow from one predicate — a
|
|
332
854
|
// trailing slot becomes `slot?:`, an interior one `slot: T | undefined`.
|
|
333
855
|
//
|
|
334
856
|
// Demonstrated omissions only. `resource.set_texture`'s buffer and
|
|
@@ -340,10 +862,6 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
|
|
|
340
862
|
'example `resource.create_texture("/my_custom_texture.texturec", tparams)`; "optional buffer of precreated pixel data"',
|
|
341
863
|
],
|
|
342
864
|
["resource.create_texture_async:param:buffer", '"optional buffer of precreated pixel data"'],
|
|
343
|
-
[
|
|
344
|
-
"resource.create_texture_async:param:callback",
|
|
345
|
-
'example `resource.create_texture_async("/my_texture.texturec", tparams, tbuffer)`',
|
|
346
|
-
],
|
|
347
865
|
[
|
|
348
866
|
"gui.new_texture:param:flip",
|
|
349
867
|
'example `gui.new_texture("orange_tx", w, h, "rgb", string.rep(orange, w * h))`',
|
|
@@ -352,16 +870,40 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
|
|
|
352
870
|
"gui.set_texture_data:param:flip",
|
|
353
871
|
'example `gui.set_texture_data("dynamic_tx", w, h, "rgb", string.rep(orange, w * h))`',
|
|
354
872
|
],
|
|
355
|
-
["sys.set_engine_throttle:param:cooldown", "example `sys.set_engine_throttle(false)`"],
|
|
356
873
|
[
|
|
357
874
|
"b2d.fixture.set_shape:param:update_mass",
|
|
358
875
|
'2 of 3 examples omit it; "The body mass is not updated unless update_mass is true"',
|
|
359
876
|
],
|
|
877
|
+
[
|
|
878
|
+
"b2d.shape.set_shape:param:update_mass",
|
|
879
|
+
'1 of 3 examples omits it; "The body mass is not updated unless update_mass is true"',
|
|
880
|
+
],
|
|
360
881
|
[
|
|
361
882
|
"b2d.shape.ray_cast:param:max_fraction",
|
|
362
883
|
'"optional maximum translation fraction, defaults to 1"',
|
|
363
884
|
],
|
|
364
885
|
["iap.buy:param:options", '"optional parameters as properties"'],
|
|
886
|
+
[
|
|
887
|
+
"go.world_to_local_position:param:url",
|
|
888
|
+
"gameobject_script.cpp:Script_WorldToLocalPosition resolves argument 2 with ResolveInstance, which takes the calling instance when it is absent",
|
|
889
|
+
],
|
|
890
|
+
[
|
|
891
|
+
"go.world_to_local_transform:param:url",
|
|
892
|
+
"gameobject_script.cpp:Script_WorldToLocalTransfrom resolves argument 2 with ResolveInstance, which takes the calling instance when it is absent",
|
|
893
|
+
],
|
|
894
|
+
[
|
|
895
|
+
"go.cancel_animations:param:url",
|
|
896
|
+
"gameobject_script.cpp:Script_CancelAnimations resolves argument 1 with ResolveURL, which takes the calling instance when it is absent",
|
|
897
|
+
],
|
|
898
|
+
[
|
|
899
|
+
"b2d.fixture.set_density:param:update_mass",
|
|
900
|
+
"script_box2d_fixture_v2.cpp:Fixture_SetDensity reads argument 4 only when lua_gettop(L) >= 4",
|
|
901
|
+
],
|
|
902
|
+
[
|
|
903
|
+
"render.set_render_target:param:render_target",
|
|
904
|
+
"render_script.cpp:RenderScript_SetRenderTarget sets the default target when lua_gettop(L) == 0",
|
|
905
|
+
],
|
|
906
|
+
["socket.newtry:param:finalizer", "socket.protect's example `local try = socket.newtry()`"],
|
|
365
907
|
...(
|
|
366
908
|
[
|
|
367
909
|
"create_distance",
|
|
@@ -383,6 +925,222 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
|
|
|
383
925
|
),
|
|
384
926
|
]);
|
|
385
927
|
|
|
928
|
+
// A parameter the ref-doc marks omissible — `[name]` or a `nil` type — that the
|
|
929
|
+
// engine binding refuses to go without, so the emitted declaration accepts a call
|
|
930
|
+
// that raises. It applies only where a ref-doc marks the slot, so it follows each
|
|
931
|
+
// target's own markup, and it overrides OPTIONAL_SLOT_CORRECTIONS: the slot is
|
|
932
|
+
// never emitted with `?`. `nil` records whether the binding still takes an
|
|
933
|
+
// explicit nil there — `lua_isnil` answers false past the stack top, so such a
|
|
934
|
+
// slot must be passed yet may be nil — which emits `slot: T | undefined`.
|
|
935
|
+
//
|
|
936
|
+
// Keyed `<element>:param:<slot>` like OPTIONAL_SLOT_CORRECTIONS. `evidence` is the
|
|
937
|
+
// C++ binding, `<file>:<cFunction>`, and what it does with the slot;
|
|
938
|
+
// `required-correction-provenance.test.ts` deletes an entry once no retained
|
|
939
|
+
// target marks the slot omissible.
|
|
940
|
+
export interface RequiredSlotCorrection {
|
|
941
|
+
readonly nil: boolean;
|
|
942
|
+
readonly evidence: string;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const COMPONENT_URL_EVIDENCE =
|
|
946
|
+
"resolves the url through GetComponentFromLua, which refuses the calling script's own url";
|
|
947
|
+
|
|
948
|
+
export const REQUIRED_SLOT_CORRECTIONS: ReadonlyMap<string, RequiredSlotCorrection> = new Map<
|
|
949
|
+
string,
|
|
950
|
+
RequiredSlotCorrection
|
|
951
|
+
>([
|
|
952
|
+
[
|
|
953
|
+
"resource.create_texture_async:param:callback",
|
|
954
|
+
{
|
|
955
|
+
nil: false,
|
|
956
|
+
evidence:
|
|
957
|
+
"script_resource.cpp:CreateTextureAsync hands argument 4 to dmScript::CreateCallback, which raises through luaL_checktype(L, 4, LUA_TFUNCTION)",
|
|
958
|
+
},
|
|
959
|
+
],
|
|
960
|
+
...(["factory", "collectionfactory"] as const).flatMap((namespace) => {
|
|
961
|
+
const file = namespace === "factory" ? "script_factory.cpp" : "script_collection_factory.cpp";
|
|
962
|
+
const prefix = namespace === "factory" ? "FactoryComp" : "CollectionFactoryComp";
|
|
963
|
+
return [
|
|
964
|
+
...(
|
|
965
|
+
[
|
|
966
|
+
["get_status", "GetStatus"],
|
|
967
|
+
["unload", "Unload"],
|
|
968
|
+
["set_prototype", "SetPrototype"],
|
|
969
|
+
["load", "Load"],
|
|
970
|
+
] as const
|
|
971
|
+
).map(
|
|
972
|
+
([name, cFunction]) =>
|
|
973
|
+
[
|
|
974
|
+
`${namespace}.${name}:param:url`,
|
|
975
|
+
{ nil: false, evidence: `${file}:${prefix}_${cFunction} ${COMPONENT_URL_EVIDENCE}` },
|
|
976
|
+
] as const,
|
|
977
|
+
),
|
|
978
|
+
[
|
|
979
|
+
`${namespace}.load:param:complete_function`,
|
|
980
|
+
{
|
|
981
|
+
nil: false,
|
|
982
|
+
evidence: `${file}:${prefix}_Load raises unless argument 2 is a function`,
|
|
983
|
+
},
|
|
984
|
+
] as const,
|
|
985
|
+
[
|
|
986
|
+
`${namespace}.set_prototype:param:prototype`,
|
|
987
|
+
{
|
|
988
|
+
nil: true,
|
|
989
|
+
evidence: `${file}:${prefix}_SetPrototype reads luaL_checkstring(L, 2) unless lua_isnil(L, 2)`,
|
|
990
|
+
},
|
|
991
|
+
] as const,
|
|
992
|
+
];
|
|
993
|
+
}),
|
|
994
|
+
[
|
|
995
|
+
"collectionproxy.set_collection:param:url",
|
|
996
|
+
{
|
|
997
|
+
nil: false,
|
|
998
|
+
evidence: `script_collectionproxy.cpp:CollectionProxy_SetCollection ${COMPONENT_URL_EVIDENCE}`,
|
|
999
|
+
},
|
|
1000
|
+
],
|
|
1001
|
+
[
|
|
1002
|
+
"collectionproxy.set_collection:param:prototype",
|
|
1003
|
+
{
|
|
1004
|
+
nil: true,
|
|
1005
|
+
evidence:
|
|
1006
|
+
"script_collectionproxy.cpp:CollectionProxy_SetCollection reads luaL_checkstring(L, 2) unless lua_isnil(L, 2)",
|
|
1007
|
+
},
|
|
1008
|
+
],
|
|
1009
|
+
[
|
|
1010
|
+
"sound.set_gain:param:gain",
|
|
1011
|
+
{ nil: false, evidence: "script_sound.cpp:Sound_SetGain reads luaL_checknumber(L, 2)" },
|
|
1012
|
+
],
|
|
1013
|
+
[
|
|
1014
|
+
"sound.set_pan:param:pan",
|
|
1015
|
+
{ nil: false, evidence: "script_sound.cpp:Sound_SetPan reads luaL_checknumber(L, 2)" },
|
|
1016
|
+
],
|
|
1017
|
+
[
|
|
1018
|
+
"resource.create_buffer:param:table",
|
|
1019
|
+
{
|
|
1020
|
+
nil: false,
|
|
1021
|
+
evidence: "script_resource.cpp:CreateBuffer checks luaL_checktype(L, 2, LUA_TTABLE)",
|
|
1022
|
+
},
|
|
1023
|
+
],
|
|
1024
|
+
[
|
|
1025
|
+
"resource.create_sound_data:param:options",
|
|
1026
|
+
{
|
|
1027
|
+
nil: false,
|
|
1028
|
+
evidence: "script_resource.cpp:CreateSoundData checks luaL_checktype(L, 2, LUA_TTABLE)",
|
|
1029
|
+
},
|
|
1030
|
+
],
|
|
1031
|
+
...(["move_above", "move_below"] as const).map(
|
|
1032
|
+
(name) =>
|
|
1033
|
+
[
|
|
1034
|
+
`gui.${name}:param:reference`,
|
|
1035
|
+
{
|
|
1036
|
+
nil: true,
|
|
1037
|
+
evidence: `gui_script.cpp:Lua${name === "move_above" ? "MoveAbove" : "MoveBelow"} reads a node from argument 2 unless lua_isnil(L, 2)`,
|
|
1038
|
+
},
|
|
1039
|
+
] as const,
|
|
1040
|
+
),
|
|
1041
|
+
[
|
|
1042
|
+
"gui.set_parent:param:parent",
|
|
1043
|
+
{
|
|
1044
|
+
nil: true,
|
|
1045
|
+
evidence: "gui_script.cpp:LuaSetParent reads a node from argument 2 unless lua_isnil(L, 2)",
|
|
1046
|
+
},
|
|
1047
|
+
],
|
|
1048
|
+
[
|
|
1049
|
+
"gui.set:param:value",
|
|
1050
|
+
{
|
|
1051
|
+
nil: true,
|
|
1052
|
+
evidence: "gui_script.cpp:LuaSet converts argument 3 with LuaToVar unless lua_isnil(L, 3)",
|
|
1053
|
+
},
|
|
1054
|
+
],
|
|
1055
|
+
[
|
|
1056
|
+
"window.set_listener:param:callback",
|
|
1057
|
+
{
|
|
1058
|
+
nil: true,
|
|
1059
|
+
evidence: "script_window.cpp:SetListener checks luaL_checkany(L, 1); nil clears the listener",
|
|
1060
|
+
},
|
|
1061
|
+
],
|
|
1062
|
+
[
|
|
1063
|
+
"html5.set_interaction_listener:param:callback",
|
|
1064
|
+
{
|
|
1065
|
+
nil: true,
|
|
1066
|
+
evidence:
|
|
1067
|
+
"script_html5_js.cpp:Html5_SetInteractionListener checks luaL_checkany(L, 1); nil clears the listener",
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
]);
|
|
1071
|
+
|
|
1072
|
+
function requiredSlotCorrection(
|
|
1073
|
+
p: ApiParameter,
|
|
1074
|
+
elementName: string,
|
|
1075
|
+
): RequiredSlotCorrection | undefined {
|
|
1076
|
+
return isMarkedOptional(p)
|
|
1077
|
+
? REQUIRED_SLOT_CORRECTIONS.get(tableSlotKey(elementName, "param", p.name))
|
|
1078
|
+
: undefined;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// A first slot upstream documents as standing in for a second argument list:
|
|
1082
|
+
// every `b2d.shape` function takes a shape handle "or pass `body, shape_index`".
|
|
1083
|
+
// The element keeps its declared signature and gains an overload with
|
|
1084
|
+
// `parameters` in that slot's place. Keyed `<namespace>:param:<slot>`, so only a
|
|
1085
|
+
// curated entry adds an overload: a reworded doc cannot add or drop one, and the
|
|
1086
|
+
// same prose in another namespace adds nothing.
|
|
1087
|
+
//
|
|
1088
|
+
// `evidence` is the upstream slot-doc fragment that names the alternative. The
|
|
1089
|
+
// emitter throws once a curated slot stops carrying it, so upstream dropping the
|
|
1090
|
+
// alternative reds here rather than shipping an overload nothing documents.
|
|
1091
|
+
export interface FirstSlotAlternative {
|
|
1092
|
+
readonly parameters: readonly ApiParameter[];
|
|
1093
|
+
readonly evidence: string;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
export const FIRST_SLOT_ALTERNATIVES: ReadonlyMap<string, FirstSlotAlternative> = new Map([
|
|
1097
|
+
[
|
|
1098
|
+
"b2d.shape:param:shape_id",
|
|
1099
|
+
{
|
|
1100
|
+
parameters: [
|
|
1101
|
+
{
|
|
1102
|
+
name: "body",
|
|
1103
|
+
doc: "The body that owns the shape.",
|
|
1104
|
+
types: ["b2Body"],
|
|
1105
|
+
isOptional: false,
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
name: "shape_index",
|
|
1109
|
+
doc: "The shape's position on that body, in the order <code>b2d.body.get_shapes</code> lists them.",
|
|
1110
|
+
types: ["number"],
|
|
1111
|
+
isOptional: false,
|
|
1112
|
+
},
|
|
1113
|
+
],
|
|
1114
|
+
evidence: "or pass <code>body, shape_index</code>",
|
|
1115
|
+
},
|
|
1116
|
+
],
|
|
1117
|
+
]);
|
|
1118
|
+
|
|
1119
|
+
// The alternative signature FIRST_SLOT_ALTERNATIVES gives `fn`, or null when
|
|
1120
|
+
// its first slot is not curated. Shared with the fidelity audit so the arity it
|
|
1121
|
+
// measures is the arity the emitter declares.
|
|
1122
|
+
export function firstSlotAlternativeOf(fn: ApiFunction, namespace: string): ApiFunction | null {
|
|
1123
|
+
const [first, ...rest] = fn.parameters;
|
|
1124
|
+
if (first === undefined) return null;
|
|
1125
|
+
const key = `${namespace}:param:${first.name}`;
|
|
1126
|
+
const alternative = FIRST_SLOT_ALTERNATIVES.get(key);
|
|
1127
|
+
if (alternative === undefined) return null;
|
|
1128
|
+
if (!first.doc.includes(alternative.evidence)) {
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
`first-slot alternative ${key}: ${fn.name} no longer documents "${alternative.evidence}"`,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
return { ...fn, parameters: [...alternative.parameters, ...rest] };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function firstSlotAlternative(
|
|
1137
|
+
prepared: PreparedFunction,
|
|
1138
|
+
namespace: string,
|
|
1139
|
+
): PreparedFunction | null {
|
|
1140
|
+
const original = firstSlotAlternativeOf(prepared.original, namespace);
|
|
1141
|
+
return original === null ? null : { ...prepared, original };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
386
1144
|
export type ConstantSlotResolution =
|
|
387
1145
|
| { readonly borrow: string }
|
|
388
1146
|
| { readonly family: string; readonly evidence: string }
|
|
@@ -782,9 +1540,17 @@ export function constantUnionAlias(fqns: readonly string[]): ConstantUnionAlias
|
|
|
782
1540
|
export function constantUnionAliasDeclarations(namespace: string): string[] {
|
|
783
1541
|
return CONSTANT_UNION_ALIASES.filter((row) => row.home === namespace)
|
|
784
1542
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
785
|
-
.map(
|
|
786
|
-
|
|
787
|
-
|
|
1543
|
+
.map((row) => unionAliasDeclaration(row.name, row.members));
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function unionAliasDeclaration(
|
|
1547
|
+
name: string,
|
|
1548
|
+
members: readonly string[],
|
|
1549
|
+
nilable: ReadonlySet<string> = new Set(),
|
|
1550
|
+
): string {
|
|
1551
|
+
const arm = (member: string) =>
|
|
1552
|
+
nilable.has(member) ? `NonNullable<typeof ${member}>` : `typeof ${member}`;
|
|
1553
|
+
return `type ${name} = ${members.map(arm).join(" | ")};`;
|
|
788
1554
|
}
|
|
789
1555
|
|
|
790
1556
|
const DOC_CONSTANT_TOKEN =
|
|
@@ -934,7 +1700,11 @@ export const HOMOGENEOUS_ARRAY_SLOTS: ReadonlyMap<string, string | readonly stri
|
|
|
934
1700
|
// (`tilemap.get_tiles` → `tiles[row][col]`).
|
|
935
1701
|
export type NestedMapping = { key: string; value: string };
|
|
936
1702
|
|
|
937
|
-
|
|
1703
|
+
// A curation applies where the slot declares the prose `table` token, or a later
|
|
1704
|
+
// spelling a newer release retypes the same slot to without fixing it
|
|
1705
|
+
// (`retypedUpstream`, matched whole): 1.13.2's `table<hash, table<string|hash, any>>`
|
|
1706
|
+
// for `collectionfactory.create`'s properties.
|
|
1707
|
+
export type TableSlotCuration = (
|
|
938
1708
|
| { kind: "mapping"; key: string; value: string | readonly TableField[] | NestedMapping }
|
|
939
1709
|
| { kind: "array"; element: string | readonly string[] }
|
|
940
1710
|
| { kind: "object"; fields: readonly TableField[] }
|
|
@@ -943,7 +1713,11 @@ export type TableSlotCuration =
|
|
|
943
1713
|
// keyed by an arbitrary name (constant/sampler/attribute). The parser recovers
|
|
944
1714
|
// the inner args fields; emit wraps them as `Record<string, { <fields> }>`,
|
|
945
1715
|
// restoring the keyed-by-name layer the flat object curation drops.
|
|
946
|
-
| { kind: "keyed-object" }
|
|
1716
|
+
| { kind: "keyed-object" }
|
|
1717
|
+
// A slot whose faithful shape no field list can carry, such as a union
|
|
1718
|
+
// discriminated on `type`. `ts` is emitted verbatim, already mapped.
|
|
1719
|
+
| { kind: "verbatim"; ts: string }
|
|
1720
|
+
) & { readonly retypedUpstream?: readonly string[] };
|
|
947
1721
|
|
|
948
1722
|
export const SOCKET_HANDLE_TOKENS = ["client", "master", "unconnected"] as const;
|
|
949
1723
|
|
|
@@ -952,8 +1726,10 @@ export const SOCKET_HANDLE_TOKENS = ["client", "master", "unconnected"] as const
|
|
|
952
1726
|
// b2d submodule curations below so a single shape is defined once.
|
|
953
1727
|
//
|
|
954
1728
|
// The shape table is a discriminated union of circle/edge/polygon/box/chain
|
|
955
|
-
// forms keyed by `type` (a b2d.shape.SHAPE_TYPE_*
|
|
956
|
-
// field is optional because only one form's
|
|
1729
|
+
// forms, plus the Box2D v3 capsule, keyed by `type` (a b2d.shape.SHAPE_TYPE_*
|
|
1730
|
+
// constant); every kind-specific field is optional because only one form's
|
|
1731
|
+
// fields are present per value. The capsule's `center1`/`center2` come from
|
|
1732
|
+
// script_box2d_shape_v3.cpp, which reads them; the ref-doc prose never names them.
|
|
957
1733
|
const B2D_SHAPE_TABLE_FIELDS: readonly TableField[] = [
|
|
958
1734
|
{ name: "type", types: ["number"] },
|
|
959
1735
|
{ name: "radius", types: ["number"], optional: true },
|
|
@@ -969,7 +1745,56 @@ const B2D_SHAPE_TABLE_FIELDS: readonly TableField[] = [
|
|
|
969
1745
|
{ name: "loop", types: ["boolean"], optional: true },
|
|
970
1746
|
{ name: "prev_vertex", types: ["vector3"], optional: true },
|
|
971
1747
|
{ name: "next_vertex", types: ["vector3"], optional: true },
|
|
1748
|
+
{ name: "center1", types: ["vector3"], optional: true },
|
|
1749
|
+
{ name: "center2", types: ["vector3"], optional: true },
|
|
972
1750
|
];
|
|
1751
|
+
// The shape table a Box2D binding reads, one arm per `type`. CheckShapeDef
|
|
1752
|
+
// (v2/script_box2d_shape_v2.cpp, v3/script_box2d_shape_v3.cpp) raises without
|
|
1753
|
+
// `type` and without the fields that type reads with luaL_checknumber or
|
|
1754
|
+
// CheckVec2; the rest go through TryGet*Field. A polygon takes `hx`/`hy` or
|
|
1755
|
+
// `vertices`. `SHAPE_TYPE_BOX` is the polygon value and `SHAPE_TYPE_EDGE` the v3
|
|
1756
|
+
// segment value, so each shares its arm. Each backend raises "Unsupported shape
|
|
1757
|
+
// type" on the kinds it does not build: `create_fixture` and `fixture.set_shape`
|
|
1758
|
+
// are registered only by v2/script_box2d_*_v2.cpp and take B2D_V2_SHAPE_TS (no
|
|
1759
|
+
// capsule, and an edge typed `SHAPE_TYPE_EDGE` with optional ghost vertices);
|
|
1760
|
+
// `create_shape` and `shape.set_shape` are registered only by
|
|
1761
|
+
// v3/script_box2d_*_v3.cpp and take B2D_V3_SHAPE_TS (no chain, and a segment
|
|
1762
|
+
// that reads no ghost vertices). `world.cast_shape` and `world.overlap_shape`
|
|
1763
|
+
// are registered by both, so they keep the full union.
|
|
1764
|
+
// The box arm leads: a v2 fixture keeps its shape type, and the runtime probe
|
|
1765
|
+
// witnesses a union by its first arm against a box collision shape.
|
|
1766
|
+
const B2D_SHAPE = "typeof b2d.shape";
|
|
1767
|
+
const B2D_BOX_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_POLYGON | ${B2D_SHAPE}.SHAPE_TYPE_BOX; hx: number; hy: number; center?: Vector3; angle?: number }`;
|
|
1768
|
+
const B2D_POLYGON_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_POLYGON | ${B2D_SHAPE}.SHAPE_TYPE_BOX; vertices: Vector3[] }`;
|
|
1769
|
+
const B2D_CIRCLE_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_CIRCLE; radius: number; center?: Vector3 }`;
|
|
1770
|
+
const B2D_CAPSULE_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_CAPSULE; radius: number; center1: Vector3; center2: Vector3 }`;
|
|
1771
|
+
const B2D_EDGE_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_EDGE | ${B2D_SHAPE}.SHAPE_TYPE_SEGMENT; v1: Vector3; v2: Vector3; v0?: Vector3; v3?: Vector3 }`;
|
|
1772
|
+
const B2D_V2_EDGE_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_EDGE; v1: Vector3; v2: Vector3; v0?: Vector3; v3?: Vector3 }`;
|
|
1773
|
+
const B2D_V3_SEGMENT_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_EDGE | ${B2D_SHAPE}.SHAPE_TYPE_SEGMENT; v1: Vector3; v2: Vector3 }`;
|
|
1774
|
+
const B2D_CHAIN_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_CHAIN; vertices: Vector3[]; loop?: boolean; prev_vertex?: Vector3; next_vertex?: Vector3 }`;
|
|
1775
|
+
const B2D_SHAPE_DEFINITION_TS = [
|
|
1776
|
+
B2D_BOX_ARM,
|
|
1777
|
+
B2D_POLYGON_ARM,
|
|
1778
|
+
B2D_CIRCLE_ARM,
|
|
1779
|
+
B2D_CAPSULE_ARM,
|
|
1780
|
+
B2D_EDGE_ARM,
|
|
1781
|
+
B2D_CHAIN_ARM,
|
|
1782
|
+
].join(" | ");
|
|
1783
|
+
const B2D_V2_SHAPE_TS = [
|
|
1784
|
+
B2D_BOX_ARM,
|
|
1785
|
+
B2D_POLYGON_ARM,
|
|
1786
|
+
B2D_CIRCLE_ARM,
|
|
1787
|
+
B2D_V2_EDGE_ARM,
|
|
1788
|
+
B2D_CHAIN_ARM,
|
|
1789
|
+
].join(" | ");
|
|
1790
|
+
const B2D_V3_SHAPE_TS = [
|
|
1791
|
+
B2D_BOX_ARM,
|
|
1792
|
+
B2D_POLYGON_ARM,
|
|
1793
|
+
B2D_CIRCLE_ARM,
|
|
1794
|
+
B2D_CAPSULE_ARM,
|
|
1795
|
+
B2D_V3_SEGMENT_ARM,
|
|
1796
|
+
].join(" | ");
|
|
1797
|
+
const B2D_SHAPE_DEFINITION: TableSlotCuration = { kind: "verbatim", ts: B2D_SHAPE_DEFINITION_TS };
|
|
973
1798
|
// A fixture info entry (index/type/sensor/density/friction/restitution/child_count).
|
|
974
1799
|
const B2D_FIXTURE_INFO_FIELDS: readonly TableField[] = [
|
|
975
1800
|
{ name: "index", types: ["number"] },
|
|
@@ -1045,8 +1870,64 @@ const PHYSICS_SHAPE_TABLE_FIELDS: readonly TableField[] = [
|
|
|
1045
1870
|
{ name: "dimensions", types: ["vector3"], optional: true },
|
|
1046
1871
|
{ name: "height", types: ["number"], optional: true },
|
|
1047
1872
|
];
|
|
1048
|
-
|
|
1049
|
-
|
|
1873
|
+
// The same record as `physics.set_shape` takes it: script_physics.cpp:
|
|
1874
|
+
// Physics_SetShape reads `type` and then each field its kind needs with
|
|
1875
|
+
// luaL_checknumber or CheckVector3, so each kind is its own arm.
|
|
1876
|
+
const PHYSICS_SHAPE_DEFINITION_TS = [
|
|
1877
|
+
"{ type: typeof physics.SHAPE_TYPE_SPHERE; diameter: number }",
|
|
1878
|
+
"{ type: typeof physics.SHAPE_TYPE_BOX; dimensions: Vector3 }",
|
|
1879
|
+
"{ type: typeof physics.SHAPE_TYPE_CAPSULE; diameter: number; height: number }",
|
|
1880
|
+
].join(" | ");
|
|
1881
|
+
|
|
1882
|
+
export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new Map<
|
|
1883
|
+
string,
|
|
1884
|
+
TableSlotCuration
|
|
1885
|
+
>([
|
|
1886
|
+
// gui_script.cpp:LuaGet and LuaSet read the `key` and `keys` options. On a
|
|
1887
|
+
// prose `table` slot TABLE_SLOT_FIELD_ADDITIONS supplies them; 1.13.2 types the
|
|
1888
|
+
// slots as an inline record and a struct that omit them.
|
|
1889
|
+
...(
|
|
1890
|
+
[
|
|
1891
|
+
["gui.get:param:options", "{ index?:integer }"],
|
|
1892
|
+
["gui.set:param:options", "gui.set_options"],
|
|
1893
|
+
] as const
|
|
1894
|
+
).map(
|
|
1895
|
+
([key, retyped]) =>
|
|
1896
|
+
[
|
|
1897
|
+
key,
|
|
1898
|
+
{
|
|
1899
|
+
kind: "verbatim",
|
|
1900
|
+
ts: "{ index?: number; key?: string | Hash; keys?: (Hash | string)[] }",
|
|
1901
|
+
retypedUpstream: [retyped],
|
|
1902
|
+
},
|
|
1903
|
+
] as const,
|
|
1904
|
+
),
|
|
1905
|
+
// buffer.create's `declaration` is "a table where each entry (table) describes
|
|
1906
|
+
// a stream", and upstream's example passes a list of stream records whose
|
|
1907
|
+
// `type` is a buffer.VALUE_TYPE_* constant.
|
|
1908
|
+
[
|
|
1909
|
+
"buffer.create:param:declaration",
|
|
1910
|
+
{
|
|
1911
|
+
kind: "array-object",
|
|
1912
|
+
fields: [
|
|
1913
|
+
{ name: "name", types: ["hash", "string"] },
|
|
1914
|
+
{ name: "type", types: ["constant"], tsType: "buffer.ValueType" },
|
|
1915
|
+
{ name: "count", types: ["number"] },
|
|
1916
|
+
],
|
|
1917
|
+
},
|
|
1918
|
+
],
|
|
1919
|
+
// collectionfactory.create's `properties` is keyed by "game object ids (hash)",
|
|
1920
|
+
// which upstream's example builds with `hash("/go")`. The `string` key keeps
|
|
1921
|
+
// the object-literal form this slot accepted as a bare `table`.
|
|
1922
|
+
[
|
|
1923
|
+
"collectionfactory.create:param:properties",
|
|
1924
|
+
{
|
|
1925
|
+
kind: "mapping",
|
|
1926
|
+
key: "hash | string",
|
|
1927
|
+
value: "table",
|
|
1928
|
+
retypedUpstream: ["table<hash, table<string|hash, any>>"],
|
|
1929
|
+
},
|
|
1930
|
+
],
|
|
1050
1931
|
["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
|
|
1051
1932
|
// font.get_info's `info` return is a `<dl>` with `path: hash` and a nested
|
|
1052
1933
|
// `fonts` field whose `<dd>` is itself a `<dl>` (`path: string`, `path_hash:
|
|
@@ -1176,8 +2057,22 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
|
|
|
1176
2057
|
],
|
|
1177
2058
|
["socket.select:param:recvt", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
|
|
1178
2059
|
["socket.select:param:sendt", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
|
|
1179
|
-
[
|
|
1180
|
-
|
|
2060
|
+
[
|
|
2061
|
+
"socket.select:return:sockets_r",
|
|
2062
|
+
{
|
|
2063
|
+
kind: "array",
|
|
2064
|
+
element: SOCKET_HANDLE_TOKENS,
|
|
2065
|
+
retypedUpstream: ["table<integer|socket_selectable, socket_selectable|integer>"],
|
|
2066
|
+
},
|
|
2067
|
+
],
|
|
2068
|
+
[
|
|
2069
|
+
"socket.select:return:sockets_w",
|
|
2070
|
+
{
|
|
2071
|
+
kind: "array",
|
|
2072
|
+
element: SOCKET_HANDLE_TOKENS,
|
|
2073
|
+
retypedUpstream: ["table<integer|socket_selectable, socket_selectable|integer>"],
|
|
2074
|
+
},
|
|
2075
|
+
],
|
|
1181
2076
|
[
|
|
1182
2077
|
"tilemap.get_tile_info:return:tile_info",
|
|
1183
2078
|
{
|
|
@@ -1269,8 +2164,10 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
|
|
|
1269
2164
|
// `headers`; the sibling `options` table stays parser-recovered.
|
|
1270
2165
|
["http.request:param:headers", { kind: "mapping", key: "string", value: "string" }],
|
|
1271
2166
|
// collectionproxy.get_resources returns "the resources, or an empty list", a
|
|
1272
|
-
// homogeneous list of resource
|
|
1273
|
-
|
|
2167
|
+
// homogeneous list of resource paths (prose only, no field list), each a
|
|
2168
|
+
// hexadecimal string: script_collectionproxy.cpp:GetResourceHashCallback pushes
|
|
2169
|
+
// every entry with lua_pushlstring.
|
|
2170
|
+
["collectionproxy.get_resources:return:resources", { kind: "array", element: "string" }],
|
|
1274
2171
|
// render.predicate's `tags` is "table of tags ... can be of either hash or
|
|
1275
2172
|
// string type", a homogeneous array whose element is the string|hash union.
|
|
1276
2173
|
["render.predicate:param:tags", { kind: "array", element: ["string", "hash"] }],
|
|
@@ -1283,6 +2180,7 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
|
|
|
1283
2180
|
[
|
|
1284
2181
|
"render.clear:param:buffers",
|
|
1285
2182
|
{
|
|
2183
|
+
retypedUpstream: ["table<graphics.BUFFER_TYPE, number|vector4>"],
|
|
1286
2184
|
kind: "mapping",
|
|
1287
2185
|
key: "graphics.BUFFER_TYPE_COLOR0_BIT | graphics.BUFFER_TYPE_DEPTH_BIT | graphics.BUFFER_TYPE_STENCIL_BIT",
|
|
1288
2186
|
value: "number | vector4",
|
|
@@ -1474,14 +2372,63 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
|
|
|
1474
2372
|
[
|
|
1475
2373
|
"b2d.body.create_fixture:param:definition",
|
|
1476
2374
|
{
|
|
2375
|
+
retypedUpstream: ["b2d.fixture_definition"],
|
|
1477
2376
|
kind: "object",
|
|
1478
2377
|
fields: [
|
|
1479
|
-
{ name: "shape", types: ["table"],
|
|
2378
|
+
{ name: "shape", types: ["table"], tsType: B2D_V2_SHAPE_TS },
|
|
1480
2379
|
{ name: "friction", types: ["number"] },
|
|
1481
2380
|
{ name: "restitution", types: ["number"] },
|
|
1482
2381
|
{ name: "density", types: ["number"] },
|
|
1483
2382
|
{ name: "sensor", types: ["boolean"] },
|
|
2383
|
+
// v2/script_box2d_fixture_v2.cpp:CheckFixtureDef reads is_sensor when
|
|
2384
|
+
// sensor is absent.
|
|
2385
|
+
{ name: "is_sensor", types: ["boolean"] },
|
|
2386
|
+
{ name: "filter", types: ["table"], fields: [...B2D_FILTER_DATA_FIELDS] },
|
|
2387
|
+
],
|
|
2388
|
+
},
|
|
2389
|
+
],
|
|
2390
|
+
// The creation-time definitions the v3 bindings document in their C++ doc
|
|
2391
|
+
// comments rather than the ref-doc: Body_CreateChain
|
|
2392
|
+
// (v3/script_box2d_chain_v3.cpp), CheckShapeCreateDef
|
|
2393
|
+
// (v3/script_box2d_shape_v3.cpp) and World_Explode
|
|
2394
|
+
// (v3/script_box2d_world_v3.cpp).
|
|
2395
|
+
[
|
|
2396
|
+
"b2d.body.create_chain:param:definition",
|
|
2397
|
+
{
|
|
2398
|
+
kind: "object",
|
|
2399
|
+
fields: [
|
|
2400
|
+
{ name: "vertices", types: ["table"], tsType: "Vector3[]" },
|
|
2401
|
+
{ name: "loop", types: ["boolean"] },
|
|
2402
|
+
{ name: "prev_vertex", types: ["vector3"] },
|
|
2403
|
+
{ name: "next_vertex", types: ["vector3"] },
|
|
2404
|
+
{ name: "friction", types: ["number"] },
|
|
2405
|
+
{ name: "restitution", types: ["number"] },
|
|
2406
|
+
{ name: "material", types: ["number"] },
|
|
1484
2407
|
{ name: "filter", types: ["table"], fields: [...B2D_FILTER_DATA_FIELDS] },
|
|
2408
|
+
{ name: "enable_sensor_events", types: ["boolean"] },
|
|
2409
|
+
],
|
|
2410
|
+
},
|
|
2411
|
+
],
|
|
2412
|
+
// The shape table sits in `definition.shape` or is the definition itself, and
|
|
2413
|
+
// a filter, when given, needs all three members CheckFilterData reads.
|
|
2414
|
+
[
|
|
2415
|
+
"b2d.body.create_shape:param:definition",
|
|
2416
|
+
{
|
|
2417
|
+
retypedUpstream: ["b2d.shape_create_definition"],
|
|
2418
|
+
kind: "verbatim",
|
|
2419
|
+
ts: `({ shape: ${B2D_V3_SHAPE_TS} } | ${B2D_V3_SHAPE_TS}) & { density?: number; friction?: number; restitution?: number; material?: number; sensor?: boolean; is_sensor?: boolean; filter?: { category_bits: number; mask_bits: number; group_index: number } }`,
|
|
2420
|
+
},
|
|
2421
|
+
],
|
|
2422
|
+
[
|
|
2423
|
+
"b2d.world.explode:param:definition",
|
|
2424
|
+
{
|
|
2425
|
+
kind: "object",
|
|
2426
|
+
fields: [
|
|
2427
|
+
{ name: "position", types: ["vector3"] },
|
|
2428
|
+
{ name: "radius", types: ["number"] },
|
|
2429
|
+
{ name: "falloff", types: ["number"] },
|
|
2430
|
+
{ name: "impulse_per_length", types: ["number"] },
|
|
2431
|
+
{ name: "mask_bits", types: ["number"] },
|
|
1485
2432
|
],
|
|
1486
2433
|
},
|
|
1487
2434
|
],
|
|
@@ -1519,10 +2466,16 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
|
|
|
1519
2466
|
["b2d.fixture.get_filter_data:return:filter", { kind: "object", fields: B2D_FILTER_DATA_FIELDS }],
|
|
1520
2467
|
["b2d.fixture.set_filter_data:param:filter", { kind: "object", fields: B2D_FILTER_DATA_FIELDS }],
|
|
1521
2468
|
["b2d.fixture.get_shape:return:shape", { kind: "object", fields: B2D_SHAPE_TABLE_FIELDS }],
|
|
1522
|
-
[
|
|
2469
|
+
[
|
|
2470
|
+
"b2d.fixture.set_shape:param:shape",
|
|
2471
|
+
{ kind: "verbatim", ts: B2D_V2_SHAPE_TS, retypedUpstream: ["b2d.shape.definition"] },
|
|
2472
|
+
],
|
|
1523
2473
|
// b2d.shape
|
|
1524
2474
|
["b2d.shape.get_shape:return:shape", { kind: "object", fields: B2D_SHAPE_TABLE_FIELDS }],
|
|
1525
|
-
[
|
|
2475
|
+
[
|
|
2476
|
+
"b2d.shape.set_shape:param:definition",
|
|
2477
|
+
{ kind: "verbatim", ts: B2D_V3_SHAPE_TS, retypedUpstream: ["b2d.shape.definition"] },
|
|
2478
|
+
],
|
|
1526
2479
|
["b2d.shape.get_mass_data:return:data", { kind: "object", fields: B2D_MASS_DATA_FIELDS }],
|
|
1527
2480
|
[
|
|
1528
2481
|
"b2d.shape.get_sensor_overlaps:return:overlaps",
|
|
@@ -1705,7 +2658,10 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
|
|
|
1705
2658
|
],
|
|
1706
2659
|
["b2d.world.overlap_aabb:return:hits", { kind: "array-object", fields: B2D_SHAPE_INFO_FIELDS }],
|
|
1707
2660
|
["b2d.world.overlap_aabb:return:stats", { kind: "object", fields: B2D_QUERY_STATS_FIELDS }],
|
|
1708
|
-
[
|
|
2661
|
+
[
|
|
2662
|
+
"b2d.world.overlap_shape:param:shape",
|
|
2663
|
+
{ ...B2D_SHAPE_DEFINITION, retypedUpstream: ["b2d.shape.definition"] },
|
|
2664
|
+
],
|
|
1709
2665
|
["b2d.world.overlap_shape:param:filter", { kind: "object", fields: B2D_QUERY_FILTER_FIELDS }],
|
|
1710
2666
|
[
|
|
1711
2667
|
"b2d.world.overlap_shape:return:fixtures",
|
|
@@ -1732,7 +2688,10 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
|
|
|
1732
2688
|
],
|
|
1733
2689
|
},
|
|
1734
2690
|
],
|
|
1735
|
-
[
|
|
2691
|
+
[
|
|
2692
|
+
"b2d.world.cast_shape:param:shape",
|
|
2693
|
+
{ ...B2D_SHAPE_DEFINITION, retypedUpstream: ["b2d.shape.definition"] },
|
|
2694
|
+
],
|
|
1736
2695
|
["b2d.world.cast_shape:param:filter", { kind: "object", fields: B2D_QUERY_FILTER_FIELDS }],
|
|
1737
2696
|
["b2d.world.cast_shape:return:hits", { kind: "array-object", fields: B2D_CAST_HIT_FIELDS }],
|
|
1738
2697
|
["b2d.world.cast_shape:return:stats", { kind: "object", fields: B2D_QUERY_STATS_FIELDS }],
|
|
@@ -1741,21 +2700,45 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
|
|
|
1741
2700
|
["b2d.world.collide_mover:param:capsule", { kind: "object", fields: B2D_CAPSULE_FIELDS }],
|
|
1742
2701
|
["b2d.world.collide_mover:param:filter", { kind: "object", fields: B2D_MOVER_FILTER_FIELDS }],
|
|
1743
2702
|
["physics.get_shape:return:table", { kind: "object", fields: PHYSICS_SHAPE_TABLE_FIELDS }],
|
|
1744
|
-
["physics.set_shape:param:table", { kind: "
|
|
2703
|
+
["physics.set_shape:param:table", { kind: "verbatim", ts: PHYSICS_SHAPE_DEFINITION_TS }],
|
|
1745
2704
|
]);
|
|
1746
2705
|
|
|
1747
2706
|
// Slot-keyed (`element:param:name`, mirroring TABLE_SLOT_CURATIONS) replacements
|
|
1748
2707
|
// for a callback parameter's recovered signature, used where the generic
|
|
1749
2708
|
// `recoverCallbackSignature` `unknown`-everywhere form leaves real engine type
|
|
1750
2709
|
// information on the table. The value is the full emitted function type. Honest
|
|
1751
|
-
// only: `window.set_listener` discriminates `event` by
|
|
2710
|
+
// only: `window.set_listener` discriminates `event` by the union of the
|
|
2711
|
+
// `WINDOW_EVENT_*` constants the surface's own `window` namespace declares, and
|
|
1752
2712
|
// its `data` is a bare record (only resize carries fields — `isWindowEvent` is the
|
|
1753
2713
|
// path to typed `data`), mirroring the `msg.post` (send, typed) / `isMessage`
|
|
1754
2714
|
// (receive, narrow) split.
|
|
2715
|
+
//
|
|
2716
|
+
// The other entries type what the slot's own prose lists and nothing more.
|
|
2717
|
+
// `http.request`'s response fields are the `<ul>` under "The response data.
|
|
2718
|
+
// Contains the fields:"; each is conditional ("if not saved on disc", "only if
|
|
2719
|
+
// option report_progress is true") except `status`, and `headers` is the same
|
|
2720
|
+
// string map the request sends. `sprite.play_flipbook`'s message is the
|
|
2721
|
+
// "Information about the completion" list. `collectionproxy.load` documents
|
|
2722
|
+
// only that the callback receives `proxy_loading`, `proxy_ready` or
|
|
2723
|
+
// `proxy_error` messages and lists no fields, so its message is the bare record
|
|
2724
|
+
// `window.set_listener`'s `data` is.
|
|
1755
2725
|
export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, string> = new Map([
|
|
1756
2726
|
[
|
|
1757
2727
|
"window.set_listener:param:callback",
|
|
1758
|
-
|
|
2728
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: emitted verbatim as a TS template literal type
|
|
2729
|
+
"(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: Record<string | number, unknown>) => void",
|
|
2730
|
+
],
|
|
2731
|
+
[
|
|
2732
|
+
"http.request:param:callback",
|
|
2733
|
+
"(self: unknown, id: Hash, response: { status: number; response?: string; headers?: LuaMap<string, string>; path?: string; error?: string; bytes_received?: number; bytes_total?: number; range_start?: number; range_end?: number; document_size?: number }) => void",
|
|
2734
|
+
],
|
|
2735
|
+
[
|
|
2736
|
+
"sprite.play_flipbook:param:complete_function",
|
|
2737
|
+
"(self: unknown, message_id: Hash, message: { current_tile: number; id: Hash }, sender: Url) => void",
|
|
2738
|
+
],
|
|
2739
|
+
[
|
|
2740
|
+
"collectionproxy.load:param:callback",
|
|
2741
|
+
"(self: unknown, message_id: Hash, message: Record<string | number, unknown>, sender: Url) => void",
|
|
1759
2742
|
],
|
|
1760
2743
|
]);
|
|
1761
2744
|
|
|
@@ -1768,9 +2751,11 @@ export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, string> = new Map
|
|
|
1768
2751
|
// hand-curated. Each geometry is the triangle data (the create-input form
|
|
1769
2752
|
// get_atlas mirrors via its cross-ref), and `vertices`/`uvs`/`indices` are
|
|
1770
2753
|
// brace-form number-lists, hence `numberList: true` (emitted `number[]`).
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
2754
|
+
// resource.create_atlas documents a geometry's `id`/`width`/`height`/pivot
|
|
2755
|
+
// members, so the three arrays join those: script_resource.cpp reads them from
|
|
2756
|
+
// each `geometries` entry. Injected onto the already-parsed field list (every
|
|
2757
|
+
// sibling stays parser-authoritative), so this is a nested-field curation, not a
|
|
2758
|
+
// whole-slot object curation.
|
|
1774
2759
|
const ATLAS_GEOMETRY_MEMBERS: readonly TableField[] = [
|
|
1775
2760
|
{ name: "vertices", types: ["table"], numberList: true },
|
|
1776
2761
|
{ name: "uvs", types: ["table"], numberList: true },
|
|
@@ -1779,6 +2764,7 @@ const ATLAS_GEOMETRY_MEMBERS: readonly TableField[] = [
|
|
|
1779
2764
|
|
|
1780
2765
|
export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]> = new Map([
|
|
1781
2766
|
["resource.set_atlas:param:table:geometries", ATLAS_GEOMETRY_MEMBERS],
|
|
2767
|
+
["resource.create_atlas:param:table:geometries", ATLAS_GEOMETRY_MEMBERS],
|
|
1782
2768
|
["resource.get_atlas:return:data:geometries", ATLAS_GEOMETRY_MEMBERS],
|
|
1783
2769
|
]);
|
|
1784
2770
|
|
|
@@ -1789,7 +2775,22 @@ export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]>
|
|
|
1789
2775
|
// parser recovers every other field faithfully but leaves that `table` token as
|
|
1790
2776
|
// a bare `Record`; this pins only the `value` field's TS type to the documented
|
|
1791
2777
|
// array union while every sibling field stays parser-authoritative. Keyed by
|
|
1792
|
-
// `<element>:<kind>:<slot>:<field>`, mirroring NESTED_FIELD_CURATIONS
|
|
2778
|
+
// `<element>:<kind>:<slot>:<field>`, mirroring NESTED_FIELD_CURATIONS; a field
|
|
2779
|
+
// one level down is `<field>.<member>`, or `<field>[].<member>` inside a list.
|
|
2780
|
+
//
|
|
2781
|
+
// The atlas animation `playback` field is documented as `constant` with the
|
|
2782
|
+
// prose "the default value is go.PLAYBACK_ONCE_FORWARD", which names the enum
|
|
2783
|
+
// the bare token cannot.
|
|
2784
|
+
//
|
|
2785
|
+
// `gui.set`'s `options.key` is documented as `hash`, while upstream's own
|
|
2786
|
+
// example sets a named font with `{ key = "my_font_name" }`.
|
|
2787
|
+
//
|
|
2788
|
+
// `render.set_render_target`'s `options.transient` is documented as `table`
|
|
2789
|
+
// with the prose "a list of buffer types", and upstream's example passes
|
|
2790
|
+
// graphics.BUFFER_TYPE_* constants.
|
|
2791
|
+
//
|
|
2792
|
+
// `resource.create_sound_data`'s `options.data` is documented as `string`, while
|
|
2793
|
+
// script_resource.cpp:CreateSoundData reads it with CheckBufferOrString.
|
|
1793
2794
|
const CONSTANT_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | (Vector4 | Matrix4)[]";
|
|
1794
2795
|
const ATTRIBUTE_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | number[]";
|
|
1795
2796
|
export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
|
|
@@ -1797,6 +2798,28 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
|
|
|
1797
2798
|
["material.set_constants:param:constants:value", CONSTANT_VALUE_TS],
|
|
1798
2799
|
["material.set_vertex_attributes:param:attributes:value", ATTRIBUTE_VALUE_TS],
|
|
1799
2800
|
["material.get_vertex_attributes:return:table:value", ATTRIBUTE_VALUE_TS],
|
|
2801
|
+
["resource.set_atlas:param:table:animations[].playback", "go.Playback"],
|
|
2802
|
+
["resource.create_atlas:param:table:animations[].playback", "go.Playback"],
|
|
2803
|
+
["resource.get_atlas:return:data:animations[].playback", "go.Playback"],
|
|
2804
|
+
["gui.set:param:options:key", "string | Hash"],
|
|
2805
|
+
["render.set_render_target:param:options:transient", "graphics.BufferType[]"],
|
|
2806
|
+
["resource.create_sound_data:param:options:data", 'string | Opaque<"buffer">'],
|
|
2807
|
+
// The same two slots where 1.13.2 types the option bag as a STRUCT, whose
|
|
2808
|
+
// members still carry the narrower upstream types.
|
|
2809
|
+
["gui.set_options:member:key", "string | Hash"],
|
|
2810
|
+
["resource.sound_data_options:member:data", 'string | Opaque<"buffer">'],
|
|
2811
|
+
// Bitmasks typed as their flag enum: the create_texture example passes
|
|
2812
|
+
// TEXTURE_USAGE_FLAG_STORAGE + TEXTURE_USAGE_FLAG_SAMPLE, a sum no single member
|
|
2813
|
+
// names, and a reported value combines them the same way.
|
|
2814
|
+
...(
|
|
2815
|
+
[
|
|
2816
|
+
"resource.texture_creation_params",
|
|
2817
|
+
"resource.texture_info",
|
|
2818
|
+
"resource.render_target_attachment_info",
|
|
2819
|
+
"material.texture_info",
|
|
2820
|
+
] as const
|
|
2821
|
+
).map((struct) => [`${struct}:member:flags`, "graphics.TEXTURE_USAGE_FLAG | number"] as const),
|
|
2822
|
+
["render.render_target_buffer_params:member:flags", "render.RENDER_TARGET_FLAG | number"],
|
|
1800
2823
|
]);
|
|
1801
2824
|
|
|
1802
2825
|
// A field the engine accepts on an option bag whose `<dl>` upstream otherwise
|
|
@@ -1805,27 +2828,56 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
|
|
|
1805
2828
|
// upstream omits, leaving the documented fields parser-authoritative so an
|
|
1806
2829
|
// upstream edit to any of them lands without a hand-maintained restatement.
|
|
1807
2830
|
//
|
|
1808
|
-
// `evidence` pins where upstream itself shows
|
|
1809
|
-
// throws once the parse recovers
|
|
1810
|
-
// here and the entry is deleted rather than
|
|
2831
|
+
// `evidence` pins where upstream itself shows each field, or the engine binding
|
|
2832
|
+
// that reads it. applyFieldAdditions throws once the parse recovers one, so
|
|
2833
|
+
// upstream documenting a field reds here and the entry is deleted rather than
|
|
2834
|
+
// re-pinned.
|
|
1811
2835
|
//
|
|
1812
2836
|
// Keyed `<element>:<kind>:<slot>`, the TABLE_SLOT_CURATIONS shape.
|
|
1813
2837
|
export interface TableFieldAddition {
|
|
1814
|
-
readonly
|
|
2838
|
+
readonly fields: readonly TableField[];
|
|
1815
2839
|
readonly evidence: readonly string[];
|
|
1816
2840
|
}
|
|
1817
2841
|
|
|
1818
|
-
|
|
2842
|
+
const PROPERTY_OPTION_KEYS: TableField = {
|
|
2843
|
+
name: "keys",
|
|
2844
|
+
types: ["table"],
|
|
2845
|
+
optional: true,
|
|
2846
|
+
tsType: "(Hash | string)[]",
|
|
2847
|
+
};
|
|
2848
|
+
|
|
2849
|
+
export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition> = new Map<
|
|
2850
|
+
string,
|
|
2851
|
+
TableFieldAddition
|
|
2852
|
+
>([
|
|
1819
2853
|
[
|
|
1820
2854
|
"resource.create_texture:param:table",
|
|
1821
2855
|
{
|
|
1822
|
-
|
|
2856
|
+
fields: [{ name: "page_count", types: ["number"], optional: true }],
|
|
1823
2857
|
evidence: [
|
|
1824
2858
|
"the array-texture example on this element creates with `page_count = 5`",
|
|
1825
2859
|
"resource.get_texture_info documents `page_count` as the texture array's page count",
|
|
1826
2860
|
],
|
|
1827
2861
|
},
|
|
1828
2862
|
],
|
|
2863
|
+
[
|
|
2864
|
+
"gui.get:param:options",
|
|
2865
|
+
{
|
|
2866
|
+
fields: [{ name: "key", types: ["string", "hash"], optional: true }, PROPERTY_OPTION_KEYS],
|
|
2867
|
+
evidence: [
|
|
2868
|
+
"gui_script.cpp:LuaGet reads its options through dmGameObject::LuaToPropertyOptions, which reads index, key and keys",
|
|
2869
|
+
],
|
|
2870
|
+
},
|
|
2871
|
+
],
|
|
2872
|
+
[
|
|
2873
|
+
"gui.set:param:options",
|
|
2874
|
+
{
|
|
2875
|
+
fields: [PROPERTY_OPTION_KEYS],
|
|
2876
|
+
evidence: [
|
|
2877
|
+
"gui_script.cpp:LuaSet reads its options through dmGameObject::LuaToPropertyOptions, which reads index, key and keys",
|
|
2878
|
+
],
|
|
2879
|
+
},
|
|
2880
|
+
],
|
|
1829
2881
|
]);
|
|
1830
2882
|
|
|
1831
2883
|
// A return-slot field upstream documents as platform-gated or nil-valued while
|
|
@@ -1917,6 +2969,247 @@ export function applyFieldOptionalityCorrections(
|
|
|
1917
2969
|
return corrected;
|
|
1918
2970
|
}
|
|
1919
2971
|
|
|
2972
|
+
// A param-side table field the engine binding refuses to go without. A `<dl>`
|
|
2973
|
+
// carries no per-field optionality, so every field of an input option bag is
|
|
2974
|
+
// emitted `?`; an entry here keeps one field required. Keyed
|
|
2975
|
+
// `<element>:param:<slot>:<field>`, the TABLE_FIELD_TYPE_OVERRIDES shape, with
|
|
2976
|
+
// `<field>.<member>` or `<field>[].<member>` one level down. A `STRUCT` member
|
|
2977
|
+
// is keyed `<struct>:member:<field>`, for a target whose slot names that struct
|
|
2978
|
+
// (`field-correction-coverage.ts` pairs the two). The value is the evidence: the
|
|
2979
|
+
// C++ binding, `<file>:<cFunction>`, and how it reads the field.
|
|
2980
|
+
const CHECK_SHAPE_DEF = "CheckShapeDef reads the shape table with luaL_checkinteger(type)";
|
|
2981
|
+
const V2_FILTER_DATA =
|
|
2982
|
+
"v2/script_box2d_fixture_v2.cpp:CheckFilterData reads each with luaL_checkinteger";
|
|
2983
|
+
const TEXTURE_PARAMS =
|
|
2984
|
+
"script_resource.cpp:CheckCreateTextureResourceParams reads CheckTableInteger";
|
|
2985
|
+
const ATLAS_ARGUMENTS =
|
|
2986
|
+
"script_resource.cpp:CheckAtlasArguments raises unless the atlas has at least one geometry and one animation";
|
|
2987
|
+
|
|
2988
|
+
const filterDataFields = (slot: string, evidence: string) =>
|
|
2989
|
+
(["category_bits", "mask_bits", "group_index"] as const).map(
|
|
2990
|
+
(field) => [`${slot}:filter.${field}`, evidence] as const,
|
|
2991
|
+
);
|
|
2992
|
+
|
|
2993
|
+
// A `STRUCT` member upstream marks required while the engine binding reads it
|
|
2994
|
+
// only when present, keyed `<struct>:member:<field>` like REQUIRED_FIELD_CORRECTIONS
|
|
2995
|
+
// and valued with the same binding evidence.
|
|
2996
|
+
export const OPTIONAL_MEMBER_CORRECTIONS: ReadonlyMap<string, string> = new Map([
|
|
2997
|
+
[
|
|
2998
|
+
"sound.stop_properties:member:play_id",
|
|
2999
|
+
"script_sound.cpp:Sound_Stop reads play_id with luaL_checknumber only when it is not nil",
|
|
3000
|
+
],
|
|
3001
|
+
]);
|
|
3002
|
+
|
|
3003
|
+
export const REQUIRED_FIELD_CORRECTIONS: ReadonlyMap<string, string> = new Map<string, string>([
|
|
3004
|
+
...(["mass", "center", "inertia"] as const).map(
|
|
3005
|
+
(field) =>
|
|
3006
|
+
[
|
|
3007
|
+
`b2d.body.set_mass_data:param:data:${field}`,
|
|
3008
|
+
"script_box2d_body_v2/v3.cpp:CheckMassData reads mass and inertia with luaL_checknumber and center with CheckVec2",
|
|
3009
|
+
] as const,
|
|
3010
|
+
),
|
|
3011
|
+
...(["category_bits", "mask_bits", "group_index"] as const).map(
|
|
3012
|
+
(field) => [`b2d.fixture.set_filter_data:param:filter:${field}`, V2_FILTER_DATA] as const,
|
|
3013
|
+
),
|
|
3014
|
+
[
|
|
3015
|
+
"b2d.body.create_fixture:param:definition:shape",
|
|
3016
|
+
`v2/script_box2d_fixture_v2.cpp:CheckFixtureDef passes definition.shape to ${CHECK_SHAPE_DEF}`,
|
|
3017
|
+
],
|
|
3018
|
+
...filterDataFields("b2d.body.create_fixture:param:definition", V2_FILTER_DATA),
|
|
3019
|
+
[
|
|
3020
|
+
"b2d.body.create_chain:param:definition:vertices",
|
|
3021
|
+
"v3/script_box2d_chain_v3.cpp:Body_CreateChain reads vertices with CheckVerticesTable, at least 2 (4 for a loop)",
|
|
3022
|
+
],
|
|
3023
|
+
...(["cast_mover", "collide_mover"] as const).flatMap((name) =>
|
|
3024
|
+
(["center1", "center2", "radius"] as const).map(
|
|
3025
|
+
(field) =>
|
|
3026
|
+
[
|
|
3027
|
+
`b2d.world.${name}:param:capsule:${field}`,
|
|
3028
|
+
"v3/script_box2d_world_v3.cpp:CheckCapsule reads center1 and center2 with CheckVec2 and radius with luaL_checknumber",
|
|
3029
|
+
] as const,
|
|
3030
|
+
),
|
|
3031
|
+
),
|
|
3032
|
+
...(["lower", "upper"] as const).map(
|
|
3033
|
+
(field) =>
|
|
3034
|
+
[
|
|
3035
|
+
`b2d.world.overlap_aabb:param:aabb:${field}`,
|
|
3036
|
+
"script_box2d_world_v2/v3.cpp:CheckAABB reads lower and upper with CheckVec2",
|
|
3037
|
+
] as const,
|
|
3038
|
+
),
|
|
3039
|
+
...(["position", "radius", "falloff", "impulse_per_length"] as const).map(
|
|
3040
|
+
(field) =>
|
|
3041
|
+
[
|
|
3042
|
+
`b2d.world.explode:param:definition:${field}`,
|
|
3043
|
+
"v3/script_box2d_world_v3.cpp:World_Explode reads position with CheckVec2 and the rest with luaL_checknumber",
|
|
3044
|
+
] as const,
|
|
3045
|
+
),
|
|
3046
|
+
[
|
|
3047
|
+
"buffer.create:param:declaration:type",
|
|
3048
|
+
"script_buffer.cpp:ParseStreamDeclaration raises on a stream with no value type",
|
|
3049
|
+
],
|
|
3050
|
+
...(["create_texture", "create_texture_async"] as const).flatMap((name) =>
|
|
3051
|
+
(["type", "format", "width", "height"] as const).map(
|
|
3052
|
+
(field) => [`resource.${name}:param:table:${field}`, TEXTURE_PARAMS] as const,
|
|
3053
|
+
),
|
|
3054
|
+
),
|
|
3055
|
+
...(["type", "format", "width", "height"] as const).map(
|
|
3056
|
+
(field) =>
|
|
3057
|
+
[
|
|
3058
|
+
`resource.set_texture:param:table:${field}`,
|
|
3059
|
+
"script_resource.cpp:SetTexture reads CheckTableInteger",
|
|
3060
|
+
] as const,
|
|
3061
|
+
),
|
|
3062
|
+
...(["create_atlas", "set_atlas"] as const).flatMap((name) => [
|
|
3063
|
+
[
|
|
3064
|
+
`resource.${name}:param:table:texture`,
|
|
3065
|
+
"script_resource.cpp:CheckTextureResource reads the texture with CheckHashOrString",
|
|
3066
|
+
] as const,
|
|
3067
|
+
[`resource.${name}:param:table:geometries`, ATLAS_ARGUMENTS] as const,
|
|
3068
|
+
[`resource.${name}:param:table:animations`, ATLAS_ARGUMENTS] as const,
|
|
3069
|
+
...(["vertices", "uvs", "indices"] as const).map(
|
|
3070
|
+
(field) =>
|
|
3071
|
+
[
|
|
3072
|
+
`resource.${name}:param:table:geometries[].${field}`,
|
|
3073
|
+
"script_resource.cpp:CheckAtlasArguments checks each geometry stream with luaL_checktype(LUA_TTABLE)",
|
|
3074
|
+
] as const,
|
|
3075
|
+
),
|
|
3076
|
+
...(["id", "width", "height"] as const).map(
|
|
3077
|
+
(field) =>
|
|
3078
|
+
[
|
|
3079
|
+
`resource.${name}:param:table:animations[].${field}`,
|
|
3080
|
+
"script_resource.cpp:CheckAtlasArguments raises on an animation with no id, width or height",
|
|
3081
|
+
] as const,
|
|
3082
|
+
),
|
|
3083
|
+
]),
|
|
3084
|
+
[
|
|
3085
|
+
"resource.create_buffer:param:table:buffer",
|
|
3086
|
+
"script_resource.cpp:CreateBuffer reads the buffer with dmScript::CheckBuffer",
|
|
3087
|
+
],
|
|
3088
|
+
[
|
|
3089
|
+
"resource.create_sound_data:param:options:data",
|
|
3090
|
+
"script_resource.cpp:CreateSoundData reads the data with CheckBufferOrString",
|
|
3091
|
+
],
|
|
3092
|
+
[
|
|
3093
|
+
"resource.sound_data_options:member:data",
|
|
3094
|
+
"script_resource.cpp:CreateSoundData reads the data with CheckBufferOrString",
|
|
3095
|
+
],
|
|
3096
|
+
]);
|
|
3097
|
+
|
|
3098
|
+
// Mark the fields REQUIRED_FIELD_CORRECTIONS names for this slot required,
|
|
3099
|
+
// leaving every other field and every type untouched. An entry keyed to this
|
|
3100
|
+
// slot that names no field throws, so a renamed field cannot leave a stale
|
|
3101
|
+
// correction looking applied. Returns a new array; never mutates its input.
|
|
3102
|
+
export function applyRequiredFieldCorrections(
|
|
3103
|
+
elementName: string,
|
|
3104
|
+
slotKind: "param" | "return" | undefined,
|
|
3105
|
+
slotName: string | undefined,
|
|
3106
|
+
fields: readonly TableField[],
|
|
3107
|
+
): TableField[] {
|
|
3108
|
+
if (slotKind !== "param" || slotName === undefined) return [...fields];
|
|
3109
|
+
const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
|
|
3110
|
+
const applied = new Set<string>();
|
|
3111
|
+
const mark = (field: TableField, path: string): TableField => {
|
|
3112
|
+
if (!REQUIRED_FIELD_CORRECTIONS.has(`${prefix}${path}`)) return field;
|
|
3113
|
+
applied.add(path);
|
|
3114
|
+
return { ...field, required: true };
|
|
3115
|
+
};
|
|
3116
|
+
const corrected = fields.map((field) => {
|
|
3117
|
+
const own = mark(field, field.name);
|
|
3118
|
+
if (own.fields === undefined) return own;
|
|
3119
|
+
const parent = `${field.name}${field.isList === true ? "[]" : ""}`;
|
|
3120
|
+
return { ...own, fields: own.fields.map((child) => mark(child, `${parent}.${child.name}`)) };
|
|
3121
|
+
});
|
|
3122
|
+
const stale = [...REQUIRED_FIELD_CORRECTIONS.keys()].filter(
|
|
3123
|
+
(key) => key.startsWith(prefix) && !applied.has(key.slice(prefix.length)),
|
|
3124
|
+
);
|
|
3125
|
+
if (stale.length > 0) {
|
|
3126
|
+
throw new Error(
|
|
3127
|
+
`required-field correction names no recovered field: ${stale.sort().join(", ")}`,
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
return corrected;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
// A param-side list field the engine binding refuses empty. Every entry emits
|
|
3134
|
+
// the list as `[E, ...E[]]`; `replaces` drops members the binding reads only as
|
|
3135
|
+
// one of several alternatives, and `alternatives` restores them as a union the
|
|
3136
|
+
// element is intersected with. Keyed `<element>:param:<slot>:<field>` or
|
|
3137
|
+
// `<struct>:member:<field>`, the REQUIRED_FIELD_CORRECTIONS shapes.
|
|
3138
|
+
export interface ListFieldCorrection {
|
|
3139
|
+
readonly evidence: string;
|
|
3140
|
+
readonly replaces?: readonly string[];
|
|
3141
|
+
readonly alternatives?: string;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
const ATLAS_FRAMES =
|
|
3145
|
+
"script_resource.cpp:CheckAtlasArguments reads a non-empty frames table, else both frame_start and frame_end with CheckFieldValue<int>";
|
|
3146
|
+
|
|
3147
|
+
const ATLAS_GEOMETRIES: ListFieldCorrection = { evidence: ATLAS_ARGUMENTS };
|
|
3148
|
+
const ATLAS_ANIMATIONS: ListFieldCorrection = {
|
|
3149
|
+
evidence: `${ATLAS_ARGUMENTS}; ${ATLAS_FRAMES}`,
|
|
3150
|
+
replaces: ["frame_start", "frame_end"],
|
|
3151
|
+
alternatives: "{ frames: [number, ...number[]] } | { frame_start: number; frame_end: number }",
|
|
3152
|
+
};
|
|
3153
|
+
|
|
3154
|
+
export const LIST_FIELD_CORRECTIONS: ReadonlyMap<string, ListFieldCorrection> = new Map<
|
|
3155
|
+
string,
|
|
3156
|
+
ListFieldCorrection
|
|
3157
|
+
>([
|
|
3158
|
+
...(["create_atlas", "set_atlas"] as const).flatMap((name) => [
|
|
3159
|
+
[`resource.${name}:param:table:geometries`, ATLAS_GEOMETRIES] as const,
|
|
3160
|
+
[`resource.${name}:param:table:animations`, ATLAS_ANIMATIONS] as const,
|
|
3161
|
+
]),
|
|
3162
|
+
["resource.atlas:member:geometries", ATLAS_GEOMETRIES],
|
|
3163
|
+
["resource.atlas:member:animations", ATLAS_ANIMATIONS],
|
|
3164
|
+
]);
|
|
3165
|
+
|
|
3166
|
+
// Apply LIST_FIELD_CORRECTIONS to this slot's fields. An entry keyed to this
|
|
3167
|
+
// slot that names no field, names a field that is not a list, or replaces a
|
|
3168
|
+
// member the field does not carry throws, so a renamed field or member cannot
|
|
3169
|
+
// leave a stale correction looking applied. Returns a new array; never mutates
|
|
3170
|
+
// its input.
|
|
3171
|
+
export function applyListFieldCorrections(
|
|
3172
|
+
elementName: string,
|
|
3173
|
+
slotKind: "param" | "return" | undefined,
|
|
3174
|
+
slotName: string | undefined,
|
|
3175
|
+
fields: readonly TableField[],
|
|
3176
|
+
): TableField[] {
|
|
3177
|
+
if (slotKind !== "param" || slotName === undefined) return [...fields];
|
|
3178
|
+
const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
|
|
3179
|
+
const applied = new Set<string>();
|
|
3180
|
+
const corrected = fields.map((field): TableField => {
|
|
3181
|
+
const key = `${prefix}${field.name}`;
|
|
3182
|
+
const correction = LIST_FIELD_CORRECTIONS.get(key);
|
|
3183
|
+
if (correction === undefined) return field;
|
|
3184
|
+
applied.add(field.name);
|
|
3185
|
+
if (field.isList !== true) {
|
|
3186
|
+
throw new Error(`list-field correction names a field that is not a list: ${key}`);
|
|
3187
|
+
}
|
|
3188
|
+
const replaces = correction.replaces ?? [];
|
|
3189
|
+
const missing = replaces.filter(
|
|
3190
|
+
(name) => !(field.fields ?? []).some((member) => member.name === name),
|
|
3191
|
+
);
|
|
3192
|
+
if (missing.length > 0) {
|
|
3193
|
+
throw new Error(`list-field correction ${key} replaces no member: ${missing.join(", ")}`);
|
|
3194
|
+
}
|
|
3195
|
+
return {
|
|
3196
|
+
...field,
|
|
3197
|
+
nonEmpty: true,
|
|
3198
|
+
...(field.fields === undefined
|
|
3199
|
+
? {}
|
|
3200
|
+
: { fields: field.fields.filter((member) => !replaces.includes(member.name)) }),
|
|
3201
|
+
...(correction.alternatives === undefined ? {} : { alternatives: correction.alternatives }),
|
|
3202
|
+
};
|
|
3203
|
+
});
|
|
3204
|
+
const stale = [...LIST_FIELD_CORRECTIONS.keys()].filter(
|
|
3205
|
+
(key) => key.startsWith(prefix) && !applied.has(key.slice(prefix.length)),
|
|
3206
|
+
);
|
|
3207
|
+
if (stale.length > 0) {
|
|
3208
|
+
throw new Error(`list-field correction names no recovered field: ${stale.sort().join(", ")}`);
|
|
3209
|
+
}
|
|
3210
|
+
return corrected;
|
|
3211
|
+
}
|
|
3212
|
+
|
|
1920
3213
|
// Append the member TABLE_SLOT_FIELD_ADDITIONS supplies for this slot, after
|
|
1921
3214
|
// every field the parser recovered. Throws on an addition whose field the parse
|
|
1922
3215
|
// already produced: upstream has documented it, and the entry is stale.
|
|
@@ -1931,10 +3224,12 @@ export function applyFieldAdditions(
|
|
|
1931
3224
|
const key = tableSlotKey(elementName, slotKind, slotName);
|
|
1932
3225
|
const addition = TABLE_SLOT_FIELD_ADDITIONS.get(key);
|
|
1933
3226
|
if (addition === undefined) return [...fields];
|
|
1934
|
-
|
|
1935
|
-
|
|
3227
|
+
for (const added of addition.fields) {
|
|
3228
|
+
if (fields.some((field) => field.name === added.name)) {
|
|
3229
|
+
throw new Error(`field addition names a recovered field: ${key}:${added.name}`);
|
|
3230
|
+
}
|
|
1936
3231
|
}
|
|
1937
|
-
return [...fields, addition.
|
|
3232
|
+
return [...fields, ...addition.fields];
|
|
1938
3233
|
}
|
|
1939
3234
|
|
|
1940
3235
|
// Pin a parser-recovered field's TS type from TABLE_FIELD_TYPE_OVERRIDES,
|
|
@@ -1949,11 +3244,17 @@ export function applyFieldTypeOverrides(
|
|
|
1949
3244
|
fields: readonly TableField[],
|
|
1950
3245
|
): TableField[] {
|
|
1951
3246
|
if (slotKind === undefined || slotName === undefined) return [...fields];
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
);
|
|
3247
|
+
const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
|
|
3248
|
+
const typed = (field: TableField, path: string): TableField => {
|
|
3249
|
+
const override = TABLE_FIELD_TYPE_OVERRIDES.get(`${prefix}${path}`);
|
|
1956
3250
|
return override === undefined ? field : { ...field, tsType: override };
|
|
3251
|
+
};
|
|
3252
|
+
return fields.map((field) => {
|
|
3253
|
+
const own = typed(field, field.name);
|
|
3254
|
+
if (own.fields === undefined) return own;
|
|
3255
|
+
const parent = `${field.name}${field.isList === true ? "[]" : ""}`;
|
|
3256
|
+
const nested = own.fields.map((child) => typed(child, `${parent}.${child.name}`));
|
|
3257
|
+
return nested.every((child, i) => child === own.fields?.[i]) ? own : { ...own, fields: nested };
|
|
1957
3258
|
});
|
|
1958
3259
|
}
|
|
1959
3260
|
|
|
@@ -2097,6 +3398,14 @@ export interface TableField {
|
|
|
2097
3398
|
isList?: boolean;
|
|
2098
3399
|
numberList?: boolean;
|
|
2099
3400
|
optional?: boolean;
|
|
3401
|
+
// Set only by REQUIRED_FIELD_CORRECTIONS: the engine raises without the field,
|
|
3402
|
+
// so it keeps no `?` even on a param-side option bag.
|
|
3403
|
+
required?: boolean;
|
|
3404
|
+
// Set only by LIST_FIELD_CORRECTIONS: the engine raises on an empty list, so
|
|
3405
|
+
// the list emits as `[E, ...E[]]`, and its element is intersected with the
|
|
3406
|
+
// alternative field sets the binding accepts.
|
|
3407
|
+
nonEmpty?: boolean;
|
|
3408
|
+
alternatives?: string;
|
|
2100
3409
|
// A hand-curated field whose faithful TS shape is not a plain token union
|
|
2101
3410
|
// (e.g. a nested vector array, or a documented value union with an array
|
|
2102
3411
|
// branch): inlineTableType emits this verbatim and the audit treats it as
|
|
@@ -2259,47 +3568,378 @@ export function parseTableFields(
|
|
|
2259
3568
|
}
|
|
2260
3569
|
}
|
|
2261
3570
|
}
|
|
2262
|
-
return null;
|
|
3571
|
+
return null;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
function isTableField(field: TableField): boolean {
|
|
3575
|
+
return field.types.length === 1 && field.types[0] === "table";
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
function groupFlattenedTableFields(fields: readonly TableField[]): TableField[] {
|
|
3579
|
+
const grouped: TableField[] = [];
|
|
3580
|
+
let open: TableField | undefined;
|
|
3581
|
+
for (const field of fields) {
|
|
3582
|
+
if (isTableField(field)) {
|
|
3583
|
+
grouped.push(field);
|
|
3584
|
+
open = field;
|
|
3585
|
+
continue;
|
|
3586
|
+
}
|
|
3587
|
+
if (open === undefined) {
|
|
3588
|
+
grouped.push(field);
|
|
3589
|
+
continue;
|
|
3590
|
+
}
|
|
3591
|
+
open.fields ??= [];
|
|
3592
|
+
open.fields.push(field);
|
|
3593
|
+
}
|
|
3594
|
+
return grouped;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
// A handle the type map widens (`constant_buffer` takes named constants) keeps
|
|
3598
|
+
// that shape, so annotating with the alias loses nothing the handle-returning
|
|
3599
|
+
// function yields.
|
|
3600
|
+
//
|
|
3601
|
+
// An engine typedef that names its aliased type: a `userdata`/`number` handle the
|
|
3602
|
+
// type map resolves (`vector`, `socket_client`) aliases that mapping, an
|
|
3603
|
+
// unresolved one keeps the brand, and any other aliased type (a union, literals, a
|
|
3604
|
+
// record) aliases the type itself.
|
|
3605
|
+
function typedefAlias(
|
|
3606
|
+
typedef: ApiTypedef,
|
|
3607
|
+
local: string,
|
|
3608
|
+
rawMapType: (token: string) => string,
|
|
3609
|
+
mapType: DefoldTypeMapper,
|
|
3610
|
+
): string {
|
|
3611
|
+
const brand = `Opaque<"${typedef.name}">`;
|
|
3612
|
+
const aliasOf = typedef.aliasOf ?? [];
|
|
3613
|
+
if (aliasOf.length > 0 && !isHandleAlias(aliasOf)) {
|
|
3614
|
+
return `type ${local} = ${unionFromTokens(aliasOf, mapType)};`;
|
|
3615
|
+
}
|
|
3616
|
+
const mapped = rawMapType(typedef.name);
|
|
3617
|
+
if (mapped.startsWith(brand)) return `type ${local} = ${mapped};`;
|
|
3618
|
+
const resolved = aliasOf.length > 0 && mapped !== "unknown" && mapped !== local;
|
|
3619
|
+
return `type ${local} = ${resolved ? mapped : brand};`;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
function isHandleAlias(aliasOf: readonly string[]): boolean {
|
|
3623
|
+
return aliasOf.length === 1 && (aliasOf[0] === "userdata" || aliasOf[0] === "number");
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// A `STRUCT` or aliasing `TYPEDEF` placed at its local path (`shape_data`,
|
|
3627
|
+
// `on_input.action`) under the module that emits it.
|
|
3628
|
+
interface TypeDeclaration {
|
|
3629
|
+
name: string;
|
|
3630
|
+
path: string;
|
|
3631
|
+
// The declaration's lines at `indent`, its keyword line led by `decl`.
|
|
3632
|
+
lines: (indent: string, decl: string) => string[];
|
|
3633
|
+
signature: string;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
// A typedef with no aliased type keeps the flat-name rule it always had; one that
|
|
3637
|
+
// names its aliased type, and every struct, sits at its name less the module
|
|
3638
|
+
// prefix, nested when that local is dotted. Typedefs come before structs, each in
|
|
3639
|
+
// document order.
|
|
3640
|
+
function collectTypeDeclarations(
|
|
3641
|
+
module: ApiModule,
|
|
3642
|
+
skip: ReadonlySet<string>,
|
|
3643
|
+
rawMapType: (token: string) => string,
|
|
3644
|
+
mapType: DefoldTypeMapper,
|
|
3645
|
+
): TypeDeclaration[] {
|
|
3646
|
+
const prefix = `${module.namespace}.`;
|
|
3647
|
+
const out: TypeDeclaration[] = [];
|
|
3648
|
+
const localOf = (name: string): string | null => {
|
|
3649
|
+
const local = stripPrefix(name, prefix);
|
|
3650
|
+
return local.split(".").every((segment) => TS_IDENTIFIER.test(segment)) ? local : null;
|
|
3651
|
+
};
|
|
3652
|
+
for (const t of module.typedefs) {
|
|
3653
|
+
const local =
|
|
3654
|
+
t.aliasOf === undefined ? (TS_IDENTIFIER.test(t.name) ? t.name : null) : localOf(t.name);
|
|
3655
|
+
if (local === null || skip.has(local)) continue;
|
|
3656
|
+
const name = local.slice(local.lastIndexOf(".") + 1);
|
|
3657
|
+
const signature = typedefAlias(t, name, rawMapType, mapType);
|
|
3658
|
+
out.push({
|
|
3659
|
+
name,
|
|
3660
|
+
path: local,
|
|
3661
|
+
lines: (indent, decl) => [`${indent}${decl}${signature}`],
|
|
3662
|
+
signature,
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
for (const struct of module.structs ?? []) {
|
|
3666
|
+
const local = localOf(struct.name);
|
|
3667
|
+
if (local === null || skip.has(local)) continue;
|
|
3668
|
+
out.push(structDeclaration(struct, local, mapType, module.structs ?? []));
|
|
3669
|
+
}
|
|
3670
|
+
return out;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
export function memberCorrectionKey(struct: string, field: string): string {
|
|
3674
|
+
return `${struct}:member:${field}`;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// A member-keyed LIST_FIELD_CORRECTIONS entry over a `T[]` member: `T` with the
|
|
3678
|
+
// replaced members omitted, rendered by the prose-field list renderer.
|
|
3679
|
+
function correctedListMemberType(
|
|
3680
|
+
struct: ApiStruct,
|
|
3681
|
+
member: ApiStructMember,
|
|
3682
|
+
correction: ListFieldCorrection,
|
|
3683
|
+
structs: readonly ApiStruct[],
|
|
3684
|
+
mapType: DefoldTypeMapper,
|
|
3685
|
+
): string {
|
|
3686
|
+
const key = memberCorrectionKey(struct.name, member.name);
|
|
3687
|
+
const element = member.type.endsWith("[]") ? member.type.slice(0, -2) : undefined;
|
|
3688
|
+
if (element === undefined || element.length === 0) {
|
|
3689
|
+
throw new Error(`list-field correction names a member that is not a list: ${key}`);
|
|
3690
|
+
}
|
|
3691
|
+
const replaces = correction.replaces ?? [];
|
|
3692
|
+
const elementStruct = structs.find((candidate) => candidate.name === element);
|
|
3693
|
+
const missing = replaces.filter(
|
|
3694
|
+
(name) => !(elementStruct?.members ?? []).some((candidate) => candidate.name === name),
|
|
3695
|
+
);
|
|
3696
|
+
if (missing.length > 0) {
|
|
3697
|
+
throw new Error(`list-field correction ${key} replaces no member: ${missing.join(", ")}`);
|
|
3698
|
+
}
|
|
3699
|
+
const mapped = mapType(element);
|
|
3700
|
+
const object =
|
|
3701
|
+
replaces.length === 0
|
|
3702
|
+
? mapped
|
|
3703
|
+
: `Omit<${mapped}, ${replaces.map((name) => JSON.stringify(name)).join(" | ")}>`;
|
|
3704
|
+
return nestedFieldType(
|
|
3705
|
+
{
|
|
3706
|
+
name: member.name,
|
|
3707
|
+
types: [],
|
|
3708
|
+
isList: true,
|
|
3709
|
+
nonEmpty: true,
|
|
3710
|
+
...(correction.alternatives === undefined ? {} : { alternatives: correction.alternatives }),
|
|
3711
|
+
},
|
|
3712
|
+
object,
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
function structDeclaration(
|
|
3717
|
+
struct: ApiStruct,
|
|
3718
|
+
path: string,
|
|
3719
|
+
mapType: DefoldTypeMapper,
|
|
3720
|
+
structs: readonly ApiStruct[],
|
|
3721
|
+
): TypeDeclaration {
|
|
3722
|
+
const name = path.slice(path.lastIndexOf(".") + 1);
|
|
3723
|
+
const memberPrefix = memberCorrectionKey(struct.name, "");
|
|
3724
|
+
const declared = new Set(struct.members.map((member) => member.name));
|
|
3725
|
+
const stale = [
|
|
3726
|
+
...REQUIRED_FIELD_CORRECTIONS.keys(),
|
|
3727
|
+
...LIST_FIELD_CORRECTIONS.keys(),
|
|
3728
|
+
...TABLE_FIELD_TYPE_OVERRIDES.keys(),
|
|
3729
|
+
...OPTIONAL_MEMBER_CORRECTIONS.keys(),
|
|
3730
|
+
].filter((key) => key.startsWith(memberPrefix) && !declared.has(key.slice(memberPrefix.length)));
|
|
3731
|
+
if (stale.length > 0) {
|
|
3732
|
+
throw new Error(`member-keyed field correction names no member: ${stale.sort().join(", ")}`);
|
|
3733
|
+
}
|
|
3734
|
+
const members = struct.members.map((member) => {
|
|
3735
|
+
const key = memberCorrectionKey(struct.name, member.name);
|
|
3736
|
+
const list = LIST_FIELD_CORRECTIONS.get(key);
|
|
3737
|
+
const retyped = TABLE_FIELD_TYPE_OVERRIDES.get(key);
|
|
3738
|
+
const optional =
|
|
3739
|
+
(member.isOptional || OPTIONAL_MEMBER_CORRECTIONS.has(key)) &&
|
|
3740
|
+
!REQUIRED_FIELD_CORRECTIONS.has(key);
|
|
3741
|
+
const type =
|
|
3742
|
+
list !== undefined
|
|
3743
|
+
? correctedListMemberType(struct, member, list, structs, mapType)
|
|
3744
|
+
: retyped !== undefined
|
|
3745
|
+
? retyped
|
|
3746
|
+
: member.type === ""
|
|
3747
|
+
? "unknown"
|
|
3748
|
+
: mapType(member.type);
|
|
3749
|
+
return {
|
|
3750
|
+
doc: member.doc,
|
|
3751
|
+
line: `${TS_IDENTIFIER.test(member.name) ? member.name : JSON.stringify(member.name)}${optional ? "?" : ""}: ${type};`,
|
|
3752
|
+
};
|
|
3753
|
+
});
|
|
3754
|
+
return {
|
|
3755
|
+
name,
|
|
3756
|
+
path,
|
|
3757
|
+
lines: (indent, decl) => {
|
|
3758
|
+
const memberIndent = `${indent}${INDENT}`;
|
|
3759
|
+
return [
|
|
3760
|
+
...summaryDocLines(struct.brief, struct.description, indent),
|
|
3761
|
+
`${indent}${decl}interface ${name} {`,
|
|
3762
|
+
...members.flatMap((member) => [
|
|
3763
|
+
...summaryDocLines(member.doc, "", memberIndent),
|
|
3764
|
+
`${memberIndent}${member.line}`,
|
|
3765
|
+
]),
|
|
3766
|
+
`${indent}}`,
|
|
3767
|
+
];
|
|
3768
|
+
},
|
|
3769
|
+
signature: `interface ${name} { ${members.map((member) => `${member.line} `).join("")}}`,
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// Nested declarations join the group at their path, which is created when no
|
|
3774
|
+
// member lives there; the flat ones are returned in name order.
|
|
3775
|
+
function placeTypeDeclarations(
|
|
3776
|
+
declarations: readonly TypeDeclaration[],
|
|
3777
|
+
nestedRoot: Map<string, NestedGroup>,
|
|
3778
|
+
): TypeDeclaration[] {
|
|
3779
|
+
const byName = (a: TypeDeclaration, b: TypeDeclaration) => a.name.localeCompare(b.name);
|
|
3780
|
+
const topLevel: TypeDeclaration[] = [];
|
|
3781
|
+
for (const declaration of declarations) {
|
|
3782
|
+
if (declaration.path === declaration.name) {
|
|
3783
|
+
topLevel.push(declaration);
|
|
3784
|
+
continue;
|
|
3785
|
+
}
|
|
3786
|
+
const group = nestedGroupAt(nestedRoot, declaration.path.split(".").slice(0, -1));
|
|
3787
|
+
group.typeDeclarations.push(declaration);
|
|
3788
|
+
group.typeDeclarations.sort(byName);
|
|
3789
|
+
}
|
|
3790
|
+
return topLevel.sort(byName);
|
|
2263
3791
|
}
|
|
2264
3792
|
|
|
2265
|
-
|
|
2266
|
-
|
|
3793
|
+
// The mappers both emit surfaces read. `mapType` is the engine vocabulary with
|
|
3794
|
+
// the surface's leaves, the module's own declared types, those `declaredTypes`
|
|
3795
|
+
// names from other modules, and constant tokens branded. `rawMapType`, read only
|
|
3796
|
+
// for a typedef's own name, leaves the declared paths out: a handle typedef
|
|
3797
|
+
// (`editor.schema`) would otherwise map to its own path and alias itself.
|
|
3798
|
+
function moduleTypeMappers(
|
|
3799
|
+
module: ApiModule,
|
|
3800
|
+
constantsByFqn: ReadonlyMap<string, ApiConstant>,
|
|
3801
|
+
options: EmitOptions | undefined,
|
|
3802
|
+
): { rawMapType: (token: string) => string; mapType: DefoldTypeMapper } {
|
|
3803
|
+
const leaves = options?.typeLeaves;
|
|
3804
|
+
const rawMapType = options?.mapType ?? mapTypeWithVocabulary({ leaves });
|
|
3805
|
+
const baseMapType =
|
|
3806
|
+
options?.mapType ??
|
|
3807
|
+
mapTypeWithVocabulary({
|
|
3808
|
+
leaves,
|
|
3809
|
+
declared: new Map([...declaredTypePaths([module]), ...(options?.declaredTypes ?? [])]),
|
|
3810
|
+
});
|
|
3811
|
+
const knownConstantFqns = options?.knownConstantFqns;
|
|
3812
|
+
const mapType: DefoldTypeMapper = (token, position) =>
|
|
3813
|
+
constantsByFqn.has(token) || knownConstantFqns?.has(token)
|
|
3814
|
+
? brandType(token, constantsByFqn.get(token))
|
|
3815
|
+
: baseMapType(token, position);
|
|
3816
|
+
return { rawMapType, mapType };
|
|
2267
3817
|
}
|
|
2268
3818
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
3819
|
+
/**
|
|
3820
|
+
* Move each `STRUCT` and aliasing `TYPEDEF` to the module whose namespace is the
|
|
3821
|
+
* longest prefix of its name (`b2d.joint.revolute_definition`, declared in the
|
|
3822
|
+
* `b2d` doc, moves to `b2d.joint`). A name no module prefixes stays with the
|
|
3823
|
+
* module that declares it. A declaration the home already declares under the
|
|
3824
|
+
* same kind and name, or that an earlier module already routed there, is
|
|
3825
|
+
* dropped, so the home's own copy wins; a type sharing a value's name
|
|
3826
|
+
* (`resource.atlas`) is untouched. A function moves the same way (1.13.2 files
|
|
3827
|
+
* `bullet3d.collision_object.get_shapes` in the shape doc), since the emitter
|
|
3828
|
+
* declares only the functions under a module's own namespace. Modules with
|
|
3829
|
+
* nothing moved in or out are returned as-is.
|
|
3830
|
+
*/
|
|
3831
|
+
export function routeTypeDeclarations(modules: readonly ApiModule[]): ApiModule[] {
|
|
3832
|
+
const homeOf = (name: string, declaring: string): string => {
|
|
3833
|
+
let home = declaring;
|
|
3834
|
+
let longest = -1;
|
|
3835
|
+
for (const { namespace } of modules) {
|
|
3836
|
+
if (name.startsWith(`${namespace}.`) && namespace.length > longest) {
|
|
3837
|
+
home = namespace;
|
|
3838
|
+
longest = namespace.length;
|
|
3839
|
+
}
|
|
2277
3840
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
3841
|
+
return home;
|
|
3842
|
+
};
|
|
3843
|
+
const incoming = new Map<
|
|
3844
|
+
string,
|
|
3845
|
+
{ structs: ApiStruct[]; typedefs: ApiTypedef[]; functions: ApiFunction[] }
|
|
3846
|
+
>();
|
|
3847
|
+
const into = (namespace: string) => {
|
|
3848
|
+
let bucket = incoming.get(namespace);
|
|
3849
|
+
if (bucket === undefined) {
|
|
3850
|
+
bucket = { structs: [], typedefs: [], functions: [] };
|
|
3851
|
+
incoming.set(namespace, bucket);
|
|
3852
|
+
}
|
|
3853
|
+
return bucket;
|
|
3854
|
+
};
|
|
3855
|
+
const kept = modules.map((module) => {
|
|
3856
|
+
const structs = (module.structs ?? []).filter((struct) => {
|
|
3857
|
+
const home = homeOf(struct.name, module.namespace);
|
|
3858
|
+
if (home === module.namespace) return true;
|
|
3859
|
+
into(home).structs.push(struct);
|
|
3860
|
+
return false;
|
|
3861
|
+
});
|
|
3862
|
+
const typedefs = module.typedefs.filter((typedef) => {
|
|
3863
|
+
if (typedef.aliasOf === undefined) return true;
|
|
3864
|
+
const home = homeOf(typedef.name, module.namespace);
|
|
3865
|
+
if (home === module.namespace) return true;
|
|
3866
|
+
into(home).typedefs.push(typedef);
|
|
3867
|
+
return false;
|
|
3868
|
+
});
|
|
3869
|
+
const functions = module.functions.filter((fn) => {
|
|
3870
|
+
const home = homeOf(fn.name, module.namespace);
|
|
3871
|
+
if (home === module.namespace) return true;
|
|
3872
|
+
into(home).functions.push(fn);
|
|
3873
|
+
return false;
|
|
3874
|
+
});
|
|
3875
|
+
return structs.length === (module.structs ?? []).length &&
|
|
3876
|
+
typedefs.length === module.typedefs.length &&
|
|
3877
|
+
functions.length === module.functions.length
|
|
3878
|
+
? module
|
|
3879
|
+
: { ...module, structs, typedefs, functions };
|
|
3880
|
+
});
|
|
3881
|
+
return kept.map((module) => {
|
|
3882
|
+
const bucket = incoming.get(module.namespace);
|
|
3883
|
+
if (bucket === undefined) return module;
|
|
3884
|
+
const unclaimed = (names: Set<string>) => (declaration: { name: string }) => {
|
|
3885
|
+
if (names.has(declaration.name)) return false;
|
|
3886
|
+
names.add(declaration.name);
|
|
3887
|
+
return true;
|
|
3888
|
+
};
|
|
3889
|
+
const structNames = new Set((module.structs ?? []).map((struct) => struct.name));
|
|
3890
|
+
const typedefNames = new Set(module.typedefs.map((typedef) => typedef.name));
|
|
3891
|
+
// A function keeps every overload one doc files, so the home's own name wins
|
|
3892
|
+
// over another doc's copy of it without collapsing the incoming overloads.
|
|
3893
|
+
const functionNames = new Set(module.functions.map((fn) => fn.name));
|
|
3894
|
+
return {
|
|
3895
|
+
...module,
|
|
3896
|
+
structs: [...(module.structs ?? []), ...bucket.structs.filter(unclaimed(structNames))],
|
|
3897
|
+
typedefs: [...module.typedefs, ...bucket.typedefs.filter(unclaimed(typedefNames))],
|
|
3898
|
+
functions: [
|
|
3899
|
+
...module.functions,
|
|
3900
|
+
...bucket.functions.filter((fn) => !functionNames.has(fn.name)),
|
|
3901
|
+
],
|
|
3902
|
+
};
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
/**
|
|
3907
|
+
* The TS path of every `ENUM`, `STRUCT` and aliasing `TYPEDEF` the (routed)
|
|
3908
|
+
* modules declare, keyed by its Lua name: the name itself when it sits under its
|
|
3909
|
+
* module's namespace, else that name nested under the module
|
|
3910
|
+
* (`on_input.action` in `go` is `go.on_input.action`).
|
|
3911
|
+
*/
|
|
3912
|
+
export function declaredTypePaths(modules: readonly ApiModule[]): Map<string, string> {
|
|
3913
|
+
const paths = new Map<string, string>();
|
|
3914
|
+
for (const module of modules) {
|
|
3915
|
+
const names = [
|
|
3916
|
+
...(module.enums ?? []).map((e) => e.name),
|
|
3917
|
+
...(module.structs ?? []).map((struct) => struct.name),
|
|
3918
|
+
...module.typedefs.filter((t) => t.aliasOf !== undefined).map((t) => t.name),
|
|
3919
|
+
];
|
|
3920
|
+
for (const name of names) {
|
|
3921
|
+
paths.set(
|
|
3922
|
+
name,
|
|
3923
|
+
name.startsWith(`${module.namespace}.`) ? name : `${module.namespace}.${name}`,
|
|
3924
|
+
);
|
|
2281
3925
|
}
|
|
2282
|
-
open.fields ??= [];
|
|
2283
|
-
open.fields.push(field);
|
|
2284
3926
|
}
|
|
2285
|
-
return
|
|
3927
|
+
return paths;
|
|
2286
3928
|
}
|
|
2287
3929
|
|
|
2288
|
-
export function emitDeclarations(
|
|
3930
|
+
export function emitDeclarations(documented: ApiModule, options?: EmitOptions): string {
|
|
3931
|
+
const module = withUndocumentedConstants(documented, options?.registeredConstants);
|
|
2289
3932
|
const prefix = `${module.namespace}.`;
|
|
2290
3933
|
|
|
2291
|
-
const
|
|
3934
|
+
const constantsByFqn = new Map(module.constants.map((c) => [c.name, c]));
|
|
2292
3935
|
const knownConstantFqns = options?.knownConstantFqns;
|
|
2293
3936
|
const translations = options?.translations ?? {};
|
|
2294
3937
|
const urlParameters = options?.urlParameters ?? [];
|
|
2295
|
-
const
|
|
2296
|
-
const mapType = (
|
|
2297
|
-
constantFqns.has(token) || knownConstantFqns?.has(token)
|
|
2298
|
-
? brandType(token)
|
|
2299
|
-
: baseMapType(token);
|
|
3938
|
+
const baseNotes = indexBaseNoteResolver(options?.indexBaseSource ?? "engine");
|
|
3939
|
+
const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
|
|
2300
3940
|
const constantTokens = constantSlotTokenResolver(
|
|
2301
3941
|
module,
|
|
2302
|
-
new Set([...
|
|
3942
|
+
new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
|
|
2303
3943
|
);
|
|
2304
3944
|
|
|
2305
3945
|
const constants = module.constants
|
|
@@ -2313,6 +3953,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2313
3953
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2314
3954
|
|
|
2315
3955
|
const functions = module.functions
|
|
3956
|
+
.filter((fn) => !isMisfiled(fn))
|
|
2316
3957
|
.map((fn) => prepareFunction(fn, prefix))
|
|
2317
3958
|
.filter((entry): entry is PreparedFunction => entry !== null)
|
|
2318
3959
|
.sort((a, b) =>
|
|
@@ -2329,6 +3970,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2329
3970
|
// segment. One or two leading identifier segments qualify; anything deeper,
|
|
2330
3971
|
// and any non-identifier segment, stays dropped.
|
|
2331
3972
|
const nestedRoot = collectNestedGroups(module, prefix);
|
|
3973
|
+
const enumAliases = collectEnumAliases(module, constants, nestedRoot);
|
|
2332
3974
|
|
|
2333
3975
|
// Colon methods (`client:send`) are FUNCTION elements named `<receiver>:<method>`
|
|
2334
3976
|
// and are NOT namespace-prefixed, so they fail the flat-identifier test in
|
|
@@ -2346,9 +3988,12 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2346
3988
|
})),
|
|
2347
3989
|
);
|
|
2348
3990
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
3991
|
+
// A typedef that also has colon methods is emitted as a method-bearing
|
|
3992
|
+
// interface below, not an opaque brand alias.
|
|
3993
|
+
const typeDeclarations = placeTypeDeclarations(
|
|
3994
|
+
collectTypeDeclarations(module, new Set(handleGroups.keys()), rawMapType, mapType),
|
|
3995
|
+
nestedRoot,
|
|
3996
|
+
);
|
|
2352
3997
|
|
|
2353
3998
|
// A re-export alias (`export { _x as x }`) switches the ambient namespace out
|
|
2354
3999
|
// of its implicit-export mode, so once any alias is present every sibling
|
|
@@ -2356,28 +4001,29 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2356
4001
|
// keyword is otherwise omitted to keep every alias-free module byte-identical.
|
|
2357
4002
|
const hasAliases =
|
|
2358
4003
|
variables.some((v) => TS_RESERVED_NAMES.has(v.name)) ||
|
|
2359
|
-
functions.some((fn) => TS_RESERVED_NAMES.has(fn.name))
|
|
4004
|
+
functions.some((fn) => TS_RESERVED_NAMES.has(fn.name)) ||
|
|
4005
|
+
[...nestedRoot.keys()].some((segment) => TS_RESERVED_NAMES.has(segment));
|
|
2360
4006
|
const decl = hasAliases ? "export " : "";
|
|
2361
4007
|
|
|
2362
4008
|
const lines: string[] = [];
|
|
2363
|
-
for (const docLine of namespaceDocLines(module)) lines.push(docLine);
|
|
4009
|
+
for (const docLine of namespaceDocLines(module, translations)) lines.push(docLine);
|
|
2364
4010
|
lines.push(`declare namespace ${module.namespace} {`);
|
|
2365
4011
|
|
|
2366
|
-
for (const
|
|
2367
|
-
// A typedef that also has colon methods is emitted as a method-bearing
|
|
2368
|
-
// interface below, not an opaque brand alias.
|
|
2369
|
-
if (handleGroups.has(t.name)) continue;
|
|
2370
|
-
lines.push(`${INDENT}${decl}type ${t.name} = Opaque<"${t.name}">;`);
|
|
2371
|
-
}
|
|
4012
|
+
for (const declaration of typeDeclarations) lines.push(...declaration.lines(INDENT, decl));
|
|
2372
4013
|
for (const aliasDecl of constantUnionAliasDeclarations(module.namespace)) {
|
|
2373
4014
|
lines.push(`${INDENT}${decl}${aliasDecl}`);
|
|
2374
4015
|
}
|
|
4016
|
+
for (const alias of enumAliases.topLevel) {
|
|
4017
|
+
lines.push(
|
|
4018
|
+
`${INDENT}${decl}${unionAliasDeclaration(alias.name, alias.members, alias.nilable)}`,
|
|
4019
|
+
);
|
|
4020
|
+
}
|
|
2375
4021
|
const handleIndent = `${INDENT}${INDENT}`;
|
|
2376
4022
|
for (const receiver of handleReceivers) {
|
|
2377
4023
|
const group = handleGroups.get(receiver) ?? [];
|
|
2378
4024
|
lines.push(`${INDENT}${decl}interface ${receiver} {`);
|
|
2379
4025
|
for (const fn of group) {
|
|
2380
|
-
for (const docLine of functionDocLines(fn.original, translations, handleIndent)) {
|
|
4026
|
+
for (const docLine of functionDocLines(fn.original, translations, baseNotes, handleIndent)) {
|
|
2381
4027
|
lines.push(docLine);
|
|
2382
4028
|
}
|
|
2383
4029
|
lines.push(
|
|
@@ -2390,7 +4036,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2390
4036
|
for (const docLine of summaryDocLines(c.original.brief, c.original.description, INDENT)) {
|
|
2391
4037
|
lines.push(docLine);
|
|
2392
4038
|
}
|
|
2393
|
-
lines.push(`${INDENT}${decl}const ${c.name}: ${
|
|
4039
|
+
lines.push(`${INDENT}${decl}const ${c.name}: ${constantType(c)};`);
|
|
2394
4040
|
}
|
|
2395
4041
|
const aliases: { internal: string; public: string }[] = [];
|
|
2396
4042
|
for (const v of variables) {
|
|
@@ -2410,11 +4056,30 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2410
4056
|
for (const fn of functions) {
|
|
2411
4057
|
const reserved = TS_RESERVED_NAMES.has(fn.name);
|
|
2412
4058
|
const emitName = aliasName(fn.name, aliases);
|
|
2413
|
-
for (const docLine of functionDocLines(fn.original, translations))
|
|
4059
|
+
for (const docLine of functionDocLines(fn.original, translations, baseNotes))
|
|
4060
|
+
lines.push(docLine);
|
|
2414
4061
|
const line = emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters);
|
|
2415
4062
|
lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
|
|
4063
|
+
const alternative = firstSlotAlternative(fn, module.namespace);
|
|
4064
|
+
if (alternative !== null) {
|
|
4065
|
+
// The overload documents its own parameters, so its hover names the base
|
|
4066
|
+
// of the index it takes. Its examples are the primary signature's.
|
|
4067
|
+
const { examples: _examples, ...overloadDoc } = alternative.original;
|
|
4068
|
+
for (const docLine of functionDocLines(overloadDoc, translations, baseNotes))
|
|
4069
|
+
lines.push(docLine);
|
|
4070
|
+
const overload = emitFunction(
|
|
4071
|
+
alternative,
|
|
4072
|
+
emitName,
|
|
4073
|
+
mapType,
|
|
4074
|
+
resolver,
|
|
4075
|
+
constantTokens,
|
|
4076
|
+
urlParameters,
|
|
4077
|
+
);
|
|
4078
|
+
lines.push(`${INDENT}${reserved ? "" : decl}${overload}`);
|
|
4079
|
+
}
|
|
2416
4080
|
}
|
|
2417
4081
|
|
|
4082
|
+
aliasReservedSegments(nestedRoot.keys(), aliases);
|
|
2418
4083
|
for (const alias of [...aliases].sort((a, b) => a.public.localeCompare(b.public))) {
|
|
2419
4084
|
lines.push(`${INDENT}export { ${alias.internal} as ${alias.public} };`);
|
|
2420
4085
|
}
|
|
@@ -2430,14 +4095,38 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2430
4095
|
// A reserved-name member inside a nested namespace gets the same recovery as
|
|
2431
4096
|
// a top-level one: emitted un-exported as `_<name>` and re-exported under the
|
|
2432
4097
|
// reserved name. The alias switches this namespace out of implicit-export mode,
|
|
2433
|
-
// so its siblings then need an explicit `export` to stay reachable.
|
|
4098
|
+
// so its siblings then need an explicit `export` to stay reachable. A
|
|
4099
|
+
// reserved segment is itself declared as `_<segment>`, merging with a
|
|
4100
|
+
// same-named function, and reached through the parent's alias.
|
|
2434
4101
|
const segmentAliases: { internal: string; public: string }[] = [];
|
|
2435
4102
|
const segmentDecl =
|
|
2436
4103
|
group.variables.some((v) => TS_RESERVED_NAMES.has(v.name)) ||
|
|
2437
|
-
group.functions.some((fn) => TS_RESERVED_NAMES.has(fn.name))
|
|
4104
|
+
group.functions.some((fn) => TS_RESERVED_NAMES.has(fn.name)) ||
|
|
4105
|
+
[...group.children.keys()].some((child) => TS_RESERVED_NAMES.has(child))
|
|
2438
4106
|
? "export "
|
|
2439
4107
|
: "";
|
|
2440
|
-
|
|
4108
|
+
const reservedSegment = TS_RESERVED_NAMES.has(segment);
|
|
4109
|
+
lines.push(
|
|
4110
|
+
`${indent}${reservedSegment ? "" : outerDecl}namespace ${reservedSegment ? `_${segment}` : segment} {`,
|
|
4111
|
+
);
|
|
4112
|
+
for (const declaration of group.typeDeclarations) {
|
|
4113
|
+
lines.push(...declaration.lines(bodyIndent, segmentDecl));
|
|
4114
|
+
}
|
|
4115
|
+
for (const alias of group.enumAliases) {
|
|
4116
|
+
lines.push(
|
|
4117
|
+
`${bodyIndent}${segmentDecl}${unionAliasDeclaration(alias.name, alias.members, alias.nilable)}`,
|
|
4118
|
+
);
|
|
4119
|
+
}
|
|
4120
|
+
for (const c of group.constants) {
|
|
4121
|
+
for (const docLine of summaryDocLines(
|
|
4122
|
+
c.original.brief,
|
|
4123
|
+
c.original.description,
|
|
4124
|
+
bodyIndent,
|
|
4125
|
+
)) {
|
|
4126
|
+
lines.push(docLine);
|
|
4127
|
+
}
|
|
4128
|
+
lines.push(`${bodyIndent}${segmentDecl}const ${c.name}: ${constantType(c)};`);
|
|
4129
|
+
}
|
|
2441
4130
|
for (const v of group.variables) {
|
|
2442
4131
|
const reserved = TS_RESERVED_NAMES.has(v.name);
|
|
2443
4132
|
const emitName = aliasName(v.name, segmentAliases);
|
|
@@ -2455,13 +4144,14 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2455
4144
|
for (const fn of group.functions) {
|
|
2456
4145
|
const reserved = TS_RESERVED_NAMES.has(fn.name);
|
|
2457
4146
|
const emitName = aliasName(fn.name, segmentAliases);
|
|
2458
|
-
for (const docLine of functionDocLines(fn.original, translations, bodyIndent)) {
|
|
4147
|
+
for (const docLine of functionDocLines(fn.original, translations, baseNotes, bodyIndent)) {
|
|
2459
4148
|
lines.push(docLine);
|
|
2460
4149
|
}
|
|
2461
4150
|
lines.push(
|
|
2462
4151
|
`${bodyIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters)}`,
|
|
2463
4152
|
);
|
|
2464
4153
|
}
|
|
4154
|
+
aliasReservedSegments(group.children.keys(), segmentAliases);
|
|
2465
4155
|
for (const alias of [...segmentAliases].sort((a, b) => a.public.localeCompare(b.public))) {
|
|
2466
4156
|
lines.push(`${bodyIndent}export { ${alias.internal} as ${alias.public} };`);
|
|
2467
4157
|
}
|
|
@@ -2475,10 +4165,12 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2475
4165
|
const members = [...module.properties].sort((a, b) => a.name.localeCompare(b.name));
|
|
2476
4166
|
lines.push(`${INDENT}${decl}interface properties {`);
|
|
2477
4167
|
for (const p of members) {
|
|
2478
|
-
for (const
|
|
2479
|
-
|
|
4168
|
+
for (const member of emitPropertyMembers(p, mapType, module.namespace)) {
|
|
4169
|
+
for (const docLine of summaryDocLines(p.brief, p.description, `${INDENT}${INDENT}`)) {
|
|
4170
|
+
lines.push(docLine);
|
|
4171
|
+
}
|
|
4172
|
+
lines.push(`${INDENT}${INDENT}${member}`);
|
|
2480
4173
|
}
|
|
2481
|
-
lines.push(`${INDENT}${INDENT}${emitPropertyMember(p, mapType, module.namespace)}`);
|
|
2482
4174
|
}
|
|
2483
4175
|
lines.push(`${INDENT}}`);
|
|
2484
4176
|
}
|
|
@@ -2488,14 +4180,22 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
|
|
|
2488
4180
|
}
|
|
2489
4181
|
|
|
2490
4182
|
/**
|
|
2491
|
-
* Every documented slot's rendered TS type, keyed
|
|
2492
|
-
* ref-doc name>`. The position is part of the key
|
|
2493
|
-
* carry several unnamed slots (`push.schedule`
|
|
2494
|
-
* key would collapse into one. Each
|
|
2495
|
-
* own `tsSignature
|
|
2496
|
-
*
|
|
4183
|
+
* Every documented slot's rendered TS type and optionality, keyed
|
|
4184
|
+
* `param|return:<position>:<raw ref-doc name>`. The position is part of the key
|
|
4185
|
+
* because a multi-return can carry several unnamed slots (`push.schedule`
|
|
4186
|
+
* renders two), which a name-only key would collapse into one. Each `type`
|
|
4187
|
+
* appears verbatim inside the symbol's own `tsSignature` as the slot's printed
|
|
4188
|
+
* operand (an interior omissible slot keeps its `| undefined` arm), and
|
|
4189
|
+
* `optional` is whether the name carries `?` there, so a consumer rendering a
|
|
4190
|
+
* slot beside that signature cannot contradict it. Returns and slots folded into
|
|
4191
|
+
* a rest element are never `optional`.
|
|
2497
4192
|
*/
|
|
2498
|
-
export
|
|
4193
|
+
export interface SlotType {
|
|
4194
|
+
readonly type: string;
|
|
4195
|
+
readonly optional: boolean;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
export type SlotTypes = Readonly<Record<string, SlotType>>;
|
|
2499
4199
|
|
|
2500
4200
|
function slotKey(kind: "param" | "return", position: number, name: string): string {
|
|
2501
4201
|
return `${kind}:${position}:${name}`;
|
|
@@ -2518,21 +4218,24 @@ export interface SymbolSignature {
|
|
|
2518
4218
|
* {@link collectNestedGroups} tree the declaration emitter walks, so neither
|
|
2519
4219
|
* surface can decide on its own which members are nested or how deep they go.
|
|
2520
4220
|
* The caller applies the same `skipFunctions` filter the `.d.ts` generation
|
|
2521
|
-
* does, so a dropped member never yields a signature.
|
|
4221
|
+
* does, so a dropped member never yields a signature. A top-level function the
|
|
4222
|
+
* declaration emits with a first-slot alternative overload carries one arm per
|
|
4223
|
+
* line, primary first, the same convention as the authored fold; its
|
|
4224
|
+
* `slotTypes` describe the primary arm only.
|
|
2522
4225
|
*/
|
|
2523
|
-
export function emitSymbolSignatures(
|
|
4226
|
+
export function emitSymbolSignatures(
|
|
4227
|
+
documented: ApiModule,
|
|
4228
|
+
options?: EmitOptions,
|
|
4229
|
+
): SymbolSignature[] {
|
|
4230
|
+
const module = withUndocumentedConstants(documented, options?.registeredConstants);
|
|
2524
4231
|
const prefix = `${module.namespace}.`;
|
|
2525
|
-
const
|
|
4232
|
+
const constantsByFqn = new Map(module.constants.map((c) => [c.name, c]));
|
|
2526
4233
|
const knownConstantFqns = options?.knownConstantFqns;
|
|
2527
4234
|
const urlParameters = options?.urlParameters ?? [];
|
|
2528
|
-
const
|
|
2529
|
-
const mapType = (token: string): string =>
|
|
2530
|
-
constantFqns.has(token) || knownConstantFqns?.has(token)
|
|
2531
|
-
? brandType(token)
|
|
2532
|
-
: baseMapType(token);
|
|
4235
|
+
const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
|
|
2533
4236
|
const constantTokens = constantSlotTokenResolver(
|
|
2534
4237
|
module,
|
|
2535
|
-
new Set([...
|
|
4238
|
+
new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
|
|
2536
4239
|
);
|
|
2537
4240
|
const resolver = buildTableDocResolver(
|
|
2538
4241
|
module.functions.map((fn) => ({
|
|
@@ -2555,25 +4258,48 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
|
|
|
2555
4258
|
const NO_SLOTS: SlotTypes = {};
|
|
2556
4259
|
|
|
2557
4260
|
for (const fn of module.functions) {
|
|
4261
|
+
if (isMisfiled(fn)) continue;
|
|
2558
4262
|
const prepared = prepareFunction(fn, prefix);
|
|
2559
4263
|
if (prepared === null) continue;
|
|
2560
|
-
const slotTypes: Record<string,
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
constantTokens,
|
|
2569
|
-
urlParameters,
|
|
2570
|
-
slotTypes,
|
|
2571
|
-
),
|
|
4264
|
+
const slotTypes: Record<string, SlotType> = {};
|
|
4265
|
+
const primary = emitFunction(
|
|
4266
|
+
prepared,
|
|
4267
|
+
emitName(prepared.name),
|
|
4268
|
+
mapType,
|
|
4269
|
+
resolver,
|
|
4270
|
+
constantTokens,
|
|
4271
|
+
urlParameters,
|
|
2572
4272
|
slotTypes,
|
|
2573
|
-
|
|
4273
|
+
);
|
|
4274
|
+
const alternative = firstSlotAlternative(prepared, module.namespace);
|
|
4275
|
+
const tsSignature =
|
|
4276
|
+
alternative === null
|
|
4277
|
+
? primary
|
|
4278
|
+
: `${primary}\n${emitFunction(
|
|
4279
|
+
alternative,
|
|
4280
|
+
emitName(prepared.name),
|
|
4281
|
+
mapType,
|
|
4282
|
+
resolver,
|
|
4283
|
+
constantTokens,
|
|
4284
|
+
urlParameters,
|
|
4285
|
+
)}`;
|
|
4286
|
+
out.push({ identity: fnIdentity(fn), tsSignature, slotTypes });
|
|
2574
4287
|
}
|
|
2575
4288
|
|
|
2576
|
-
const
|
|
4289
|
+
const nestedRoot = collectNestedGroups(module, prefix);
|
|
4290
|
+
const nested = flattenNestedGroups(nestedRoot);
|
|
4291
|
+
for (const c of nested.constants) {
|
|
4292
|
+
out.push({
|
|
4293
|
+
identity: {
|
|
4294
|
+
namespace: module.namespace,
|
|
4295
|
+
kind: "CONSTANT",
|
|
4296
|
+
name: c.fqn,
|
|
4297
|
+
signature: "",
|
|
4298
|
+
},
|
|
4299
|
+
tsSignature: `const ${c.name}: ${constantType(c)};`,
|
|
4300
|
+
slotTypes: NO_SLOTS,
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
2577
4303
|
for (const v of nested.variables) {
|
|
2578
4304
|
out.push({
|
|
2579
4305
|
identity: {
|
|
@@ -2587,7 +4313,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
|
|
|
2587
4313
|
});
|
|
2588
4314
|
}
|
|
2589
4315
|
for (const fn of nested.functions) {
|
|
2590
|
-
const slotTypes: Record<string,
|
|
4316
|
+
const slotTypes: Record<string, SlotType> = {};
|
|
2591
4317
|
out.push({
|
|
2592
4318
|
identity: fnIdentity(fn.original),
|
|
2593
4319
|
tsSignature: emitFunction(
|
|
@@ -2606,7 +4332,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
|
|
|
2606
4332
|
const handleGroups = collectHandleMethodGroups(module);
|
|
2607
4333
|
for (const group of handleGroups.values()) {
|
|
2608
4334
|
for (const prepared of group) {
|
|
2609
|
-
const slotTypes: Record<string,
|
|
4335
|
+
const slotTypes: Record<string, SlotType> = {};
|
|
2610
4336
|
out.push({
|
|
2611
4337
|
identity: fnIdentity(prepared.original),
|
|
2612
4338
|
tsSignature: emitMethod(
|
|
@@ -2622,12 +4348,14 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
|
|
|
2622
4348
|
}
|
|
2623
4349
|
}
|
|
2624
4350
|
|
|
4351
|
+
const constants: PreparedConstant[] = [];
|
|
2625
4352
|
for (const c of module.constants) {
|
|
2626
4353
|
const prepared = prepareConstant(c, prefix);
|
|
2627
4354
|
if (prepared === null) continue;
|
|
4355
|
+
constants.push(prepared);
|
|
2628
4356
|
out.push({
|
|
2629
4357
|
identity: { namespace: module.namespace, kind: "CONSTANT", name: c.name, signature: "" },
|
|
2630
|
-
tsSignature: `const ${prepared.name}: ${
|
|
4358
|
+
tsSignature: `const ${prepared.name}: ${constantType(prepared)};`,
|
|
2631
4359
|
slotTypes: NO_SLOTS,
|
|
2632
4360
|
});
|
|
2633
4361
|
}
|
|
@@ -2642,19 +4370,34 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
|
|
|
2642
4370
|
});
|
|
2643
4371
|
}
|
|
2644
4372
|
|
|
4373
|
+
// A key-range property emits several members that share one type; its entry
|
|
4374
|
+
// carries the first, which is verbatim in the `.d.ts` and is what the `/api`
|
|
4375
|
+
// render layer reduces to `<placeholder>: <type>`.
|
|
2645
4376
|
for (const p of module.properties) {
|
|
4377
|
+
const [firstMember = ""] = emitPropertyMembers(p, mapType, module.namespace);
|
|
2646
4378
|
out.push({
|
|
2647
4379
|
identity: { namespace: module.namespace, kind: "PROPERTY", name: p.name, signature: "" },
|
|
2648
|
-
tsSignature:
|
|
4380
|
+
tsSignature: firstMember,
|
|
2649
4381
|
slotTypes: NO_SLOTS,
|
|
2650
4382
|
});
|
|
2651
4383
|
}
|
|
2652
4384
|
|
|
2653
|
-
|
|
2654
|
-
|
|
4385
|
+
// A struct signs as its interface on one line; each member line is verbatim in
|
|
4386
|
+
// the declarations.
|
|
4387
|
+
for (const declaration of collectTypeDeclarations(
|
|
4388
|
+
module,
|
|
4389
|
+
new Set(handleGroups.keys()),
|
|
4390
|
+
rawMapType,
|
|
4391
|
+
mapType,
|
|
4392
|
+
)) {
|
|
2655
4393
|
out.push({
|
|
2656
|
-
identity: {
|
|
2657
|
-
|
|
4394
|
+
identity: {
|
|
4395
|
+
namespace: module.namespace,
|
|
4396
|
+
kind: "TYPEDEF",
|
|
4397
|
+
name: declaration.path,
|
|
4398
|
+
signature: "",
|
|
4399
|
+
},
|
|
4400
|
+
tsSignature: declaration.signature,
|
|
2658
4401
|
slotTypes: NO_SLOTS,
|
|
2659
4402
|
});
|
|
2660
4403
|
}
|
|
@@ -2665,9 +4408,18 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
|
|
|
2665
4408
|
for (const row of CONSTANT_UNION_ALIASES.filter((r) => r.home === module.namespace)) {
|
|
2666
4409
|
out.push({
|
|
2667
4410
|
identity: { namespace: module.namespace, kind: "TYPEDEF", name: row.name, signature: "" },
|
|
2668
|
-
tsSignature:
|
|
2669
|
-
|
|
2670
|
-
|
|
4411
|
+
tsSignature: unionAliasDeclaration(row.name, row.members),
|
|
4412
|
+
slotTypes: NO_SLOTS,
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
// An enum alias is keyed by its local path (`STATUS`, `ui.COLOR`), as the
|
|
4417
|
+
// constant-union aliases above are keyed by their local name.
|
|
4418
|
+
const enumAliases = collectEnumAliases(module, constants, nestedRoot);
|
|
4419
|
+
for (const alias of enumAliases.all) {
|
|
4420
|
+
out.push({
|
|
4421
|
+
identity: { namespace: module.namespace, kind: "TYPEDEF", name: alias.path, signature: "" },
|
|
4422
|
+
tsSignature: unionAliasDeclaration(alias.name, alias.members, alias.nilable),
|
|
2671
4423
|
slotTypes: NO_SLOTS,
|
|
2672
4424
|
});
|
|
2673
4425
|
}
|
|
@@ -2694,35 +4446,67 @@ interface PreparedVariable {
|
|
|
2694
4446
|
// One level of the nested-member tree: the members declared directly under a
|
|
2695
4447
|
// segment, plus the segments declared beneath it.
|
|
2696
4448
|
interface NestedGroup {
|
|
4449
|
+
constants: PreparedConstant[];
|
|
2697
4450
|
variables: PreparedVariable[];
|
|
2698
4451
|
functions: PreparedFunction[];
|
|
4452
|
+
enumAliases: EnumAlias[];
|
|
4453
|
+
typeDeclarations: TypeDeclaration[];
|
|
2699
4454
|
children: Map<string, NestedGroup>;
|
|
2700
4455
|
}
|
|
2701
4456
|
|
|
4457
|
+
// One `ENUM`'s union alias: `name` is the declared identifier, `path` the local
|
|
4458
|
+
// path under the module namespace (`ui.COLOR`), `members` the member FQNs.
|
|
4459
|
+
interface EnumAlias {
|
|
4460
|
+
name: string;
|
|
4461
|
+
path: string;
|
|
4462
|
+
members: readonly string[];
|
|
4463
|
+
// Members typed `<ENUM>|nil`, whose arm drops the `undefined` their `const`
|
|
4464
|
+
// admits, as a slot naming one does.
|
|
4465
|
+
nilable: ReadonlySet<string>;
|
|
4466
|
+
}
|
|
4467
|
+
|
|
2702
4468
|
// A stripped local carrying one or two leading identifier segments before the
|
|
2703
4469
|
// member name (`dns.toip`, `COLOR.TEXT`, `schema.integer`). Three levels deep is
|
|
2704
4470
|
// beyond anything the vendored documents describe, so it stays dropped rather
|
|
2705
4471
|
// than half-emitted.
|
|
2706
4472
|
const NESTED_MEMBER_LOCAL = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*){1,2}$/;
|
|
2707
4473
|
|
|
4474
|
+
// The group at `segments` under `root`, creating each missing level.
|
|
4475
|
+
function nestedGroupAt(root: Map<string, NestedGroup>, segments: readonly string[]): NestedGroup {
|
|
4476
|
+
let level = root;
|
|
4477
|
+
let group: NestedGroup | undefined;
|
|
4478
|
+
for (const segment of segments) {
|
|
4479
|
+
let next = level.get(segment);
|
|
4480
|
+
if (next === undefined) {
|
|
4481
|
+
next = {
|
|
4482
|
+
constants: [],
|
|
4483
|
+
variables: [],
|
|
4484
|
+
functions: [],
|
|
4485
|
+
enumAliases: [],
|
|
4486
|
+
typeDeclarations: [],
|
|
4487
|
+
children: new Map(),
|
|
4488
|
+
};
|
|
4489
|
+
level.set(segment, next);
|
|
4490
|
+
}
|
|
4491
|
+
group = next;
|
|
4492
|
+
level = next.children;
|
|
4493
|
+
}
|
|
4494
|
+
return group as NestedGroup;
|
|
4495
|
+
}
|
|
4496
|
+
|
|
2708
4497
|
function collectNestedGroups(module: ApiModule, prefix: string): Map<string, NestedGroup> {
|
|
2709
4498
|
const root = new Map<string, NestedGroup>();
|
|
2710
|
-
const groupAt = (segments: readonly string[]): NestedGroup =>
|
|
2711
|
-
let level = root;
|
|
2712
|
-
let group: NestedGroup | undefined;
|
|
2713
|
-
for (const segment of segments) {
|
|
2714
|
-
let next = level.get(segment);
|
|
2715
|
-
if (next === undefined) {
|
|
2716
|
-
next = { variables: [], functions: [], children: new Map() };
|
|
2717
|
-
level.set(segment, next);
|
|
2718
|
-
}
|
|
2719
|
-
group = next;
|
|
2720
|
-
level = next.children;
|
|
2721
|
-
}
|
|
2722
|
-
return group as NestedGroup;
|
|
2723
|
-
};
|
|
4499
|
+
const groupAt = (segments: readonly string[]): NestedGroup => nestedGroupAt(root, segments);
|
|
2724
4500
|
const pathOf = (local: string): string[] => local.split(".").slice(0, -1);
|
|
2725
4501
|
|
|
4502
|
+
for (const c of module.constants) {
|
|
4503
|
+
const local = stripPrefix(c.name, prefix);
|
|
4504
|
+
if (!NESTED_MEMBER_LOCAL.test(local)) continue;
|
|
4505
|
+
const segments = pathOf(local);
|
|
4506
|
+
const prepared = prepareConstant(c, `${module.namespace}.${segments.join(".")}.`);
|
|
4507
|
+
if (prepared === null) continue;
|
|
4508
|
+
groupAt(segments).constants.push(prepared);
|
|
4509
|
+
}
|
|
2726
4510
|
for (const v of module.variables) {
|
|
2727
4511
|
const local = stripPrefix(v.name, prefix);
|
|
2728
4512
|
if (!NESTED_MEMBER_LOCAL.test(local)) continue;
|
|
@@ -2742,6 +4526,7 @@ function collectNestedGroups(module: ApiModule, prefix: string): Map<string, Nes
|
|
|
2742
4526
|
|
|
2743
4527
|
const sortLevel = (level: Map<string, NestedGroup>): void => {
|
|
2744
4528
|
for (const group of level.values()) {
|
|
4529
|
+
group.constants.sort((a, b) => a.name.localeCompare(b.name));
|
|
2745
4530
|
group.variables.sort((a, b) => a.name.localeCompare(b.name));
|
|
2746
4531
|
group.functions.sort((a, b) =>
|
|
2747
4532
|
a.name === b.name
|
|
@@ -2760,21 +4545,71 @@ function collectNestedGroups(module: ApiModule, prefix: string): Map<string, Nes
|
|
|
2760
4545
|
// children). Callers that need no per-segment container read the tree through
|
|
2761
4546
|
// this rather than re-deriving which members are nested and how deep.
|
|
2762
4547
|
function flattenNestedGroups(level: ReadonlyMap<string, NestedGroup>): {
|
|
4548
|
+
constants: PreparedConstant[];
|
|
2763
4549
|
variables: PreparedVariable[];
|
|
2764
4550
|
functions: PreparedFunction[];
|
|
2765
4551
|
} {
|
|
4552
|
+
const constants: PreparedConstant[] = [];
|
|
2766
4553
|
const variables: PreparedVariable[] = [];
|
|
2767
4554
|
const functions: PreparedFunction[] = [];
|
|
2768
4555
|
const walk = (current: ReadonlyMap<string, NestedGroup>): void => {
|
|
2769
4556
|
for (const segment of [...current.keys()].sort((a, b) => a.localeCompare(b))) {
|
|
2770
4557
|
const group = current.get(segment) as NestedGroup;
|
|
4558
|
+
constants.push(...group.constants);
|
|
2771
4559
|
variables.push(...group.variables);
|
|
2772
4560
|
functions.push(...group.functions);
|
|
2773
4561
|
walk(group.children);
|
|
2774
4562
|
}
|
|
2775
4563
|
};
|
|
2776
4564
|
walk(level);
|
|
2777
|
-
return { variables, functions };
|
|
4565
|
+
return { constants, variables, functions };
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
// Each `ENUM`'s union alias, placed where its name lands: a flat local
|
|
4569
|
+
// (`STATUS`) at the top of the namespace, a nested one (`ui.COLOR`) in the
|
|
4570
|
+
// nested group `ui`, which the tree already holds when the members are nested
|
|
4571
|
+
// beside it. An enum is aliased only when every member is declared, so the
|
|
4572
|
+
// alias never names a constant the module does not emit. The nested groups
|
|
4573
|
+
// gain their aliases in place; `topLevel` and `all` are in name order.
|
|
4574
|
+
function collectEnumAliases(
|
|
4575
|
+
module: ApiModule,
|
|
4576
|
+
topLevelConstants: readonly PreparedConstant[],
|
|
4577
|
+
nestedRoot: Map<string, NestedGroup>,
|
|
4578
|
+
): { topLevel: EnumAlias[]; all: EnumAlias[] } {
|
|
4579
|
+
const prefix = `${module.namespace}.`;
|
|
4580
|
+
const declared = new Set([
|
|
4581
|
+
...topLevelConstants.map((c) => c.fqn),
|
|
4582
|
+
...flattenNestedGroups(nestedRoot).constants.map((c) => c.fqn),
|
|
4583
|
+
]);
|
|
4584
|
+
const nilable = new Set(module.constants.filter((c) => c.nilable).map((c) => c.name));
|
|
4585
|
+
const topLevel: EnumAlias[] = [];
|
|
4586
|
+
const all: EnumAlias[] = [];
|
|
4587
|
+
const enums = [...(module.enums ?? [])].sort((a, b) => a.name.localeCompare(b.name));
|
|
4588
|
+
for (const e of enums) {
|
|
4589
|
+
if (e.members.length === 0 || !e.members.every((m) => declared.has(m))) continue;
|
|
4590
|
+
const path = stripPrefix(e.name, prefix);
|
|
4591
|
+
if (TS_IDENTIFIER.test(path)) {
|
|
4592
|
+
const alias = { name: path, path, members: e.members, nilable };
|
|
4593
|
+
topLevel.push(alias);
|
|
4594
|
+
all.push(alias);
|
|
4595
|
+
continue;
|
|
4596
|
+
}
|
|
4597
|
+
if (!NESTED_MEMBER_LOCAL.test(path)) continue;
|
|
4598
|
+
const segments = path.split(".");
|
|
4599
|
+
const name = segments.pop() as string;
|
|
4600
|
+
let level = nestedRoot;
|
|
4601
|
+
let group: NestedGroup | undefined;
|
|
4602
|
+
for (const segment of segments) {
|
|
4603
|
+
group = level.get(segment);
|
|
4604
|
+
if (group === undefined) break;
|
|
4605
|
+
level = group.children;
|
|
4606
|
+
}
|
|
4607
|
+
if (group === undefined) continue;
|
|
4608
|
+
const alias = { name, path, members: e.members, nilable };
|
|
4609
|
+
group.enumAliases.push(alias);
|
|
4610
|
+
all.push(alias);
|
|
4611
|
+
}
|
|
4612
|
+
return { topLevel, all };
|
|
2778
4613
|
}
|
|
2779
4614
|
|
|
2780
4615
|
function prepareFunction(fn: ApiFunction, prefix: string): PreparedFunction | null {
|
|
@@ -2820,8 +4655,137 @@ function prepareConstant(c: ApiConstant, prefix: string): PreparedConstant | nul
|
|
|
2820
4655
|
return { name: stripped, fqn: c.name, original: c };
|
|
2821
4656
|
}
|
|
2822
4657
|
|
|
2823
|
-
|
|
2824
|
-
|
|
4658
|
+
// Where the engine registers a constant as a string: the vendored binding file
|
|
4659
|
+
// and the call there whose macro pushes the literal.
|
|
4660
|
+
export interface StringConstantSource {
|
|
4661
|
+
readonly binding: string;
|
|
4662
|
+
readonly macro: string;
|
|
4663
|
+
readonly call: string;
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4666
|
+
const GUI_SCRIPT = "engine/gui/src/gui_script.cpp";
|
|
4667
|
+
const IMAGE_SCRIPT = "engine/gamesys/src/gamesys/scripts/script_image.cpp";
|
|
4668
|
+
|
|
4669
|
+
// Constants the engine registers with `lua_pushliteral`, keyed by FQN. The
|
|
4670
|
+
// ref-docs give constants no type, and every other constant is a number;
|
|
4671
|
+
// `string-constant-provenance.test.ts` reds when a vendored binding stops
|
|
4672
|
+
// pushing one as a string.
|
|
4673
|
+
export const STRING_CONSTANTS: ReadonlyMap<string, StringConstantSource> = new Map([
|
|
4674
|
+
...[
|
|
4675
|
+
"position",
|
|
4676
|
+
"rotation",
|
|
4677
|
+
"euler",
|
|
4678
|
+
"scale",
|
|
4679
|
+
"color",
|
|
4680
|
+
"outline",
|
|
4681
|
+
"shadow",
|
|
4682
|
+
"size",
|
|
4683
|
+
"fill_angle",
|
|
4684
|
+
"inner_radius",
|
|
4685
|
+
"leading",
|
|
4686
|
+
"tracking",
|
|
4687
|
+
"slice9",
|
|
4688
|
+
].map((prop): [string, StringConstantSource] => {
|
|
4689
|
+
const name = prop.toUpperCase();
|
|
4690
|
+
const call = `SETPROP(${prop}, ${name})`;
|
|
4691
|
+
return [`gui.PROP_${name}`, { binding: GUI_SCRIPT, macro: "SETPROP", call }];
|
|
4692
|
+
}),
|
|
4693
|
+
...(
|
|
4694
|
+
[
|
|
4695
|
+
["TYPE_RGB", "rgb"],
|
|
4696
|
+
["TYPE_RGBA", "rgba"],
|
|
4697
|
+
["TYPE_LUMINANCE", "l"],
|
|
4698
|
+
["TYPE_LUMINANCE_ALPHA", "la"],
|
|
4699
|
+
] as const
|
|
4700
|
+
).map(([name, value]): [string, StringConstantSource] => [
|
|
4701
|
+
`image.${name}`,
|
|
4702
|
+
{ binding: IMAGE_SCRIPT, macro: "SETCONSTANT", call: `SETCONSTANT(${name}, "${value}")` },
|
|
4703
|
+
]),
|
|
4704
|
+
]);
|
|
4705
|
+
|
|
4706
|
+
// Documented constants the engine never registers, keyed by FQN with the
|
|
4707
|
+
// reason, so Lua reads each as nil. `nil-constant-provenance.test.ts` pins the
|
|
4708
|
+
// keys to the accepted `constant-missing-binding` binding verdicts.
|
|
4709
|
+
export const NIL_CONSTANTS: ReadonlyMap<string, string> = new Map([
|
|
4710
|
+
[
|
|
4711
|
+
"render.RENDER_TARGET_DEFAULT",
|
|
4712
|
+
"render_script.cpp registers no such field; render.set_render_target reads the nil as the default target",
|
|
4713
|
+
],
|
|
4714
|
+
]);
|
|
4715
|
+
|
|
4716
|
+
// Constants the engine registers but some release's ref-doc omits, keyed by FQN
|
|
4717
|
+
// with the doc the emit carries. A surface gains an entry only where its ref-doc
|
|
4718
|
+
// leaves the constant out and its engine bindings register it.
|
|
4719
|
+
// `undocumented-constant-provenance.test.ts` reds an entry no committed target
|
|
4720
|
+
// still needs. `sameValueAs` names the documented constant the engine registers
|
|
4721
|
+
// with the same value; where the surface declares it, the back-fill takes its
|
|
4722
|
+
// type, so a value compares equal to either spelling.
|
|
4723
|
+
export const UNDOCUMENTED_CONSTANTS: ReadonlyMap<
|
|
4724
|
+
string,
|
|
4725
|
+
{ readonly doc: string; readonly sameValueAs?: string }
|
|
4726
|
+
> = new Map([
|
|
4727
|
+
[
|
|
4728
|
+
"render.CONTEXT_EVENT_CONTEXT_LOST",
|
|
4729
|
+
{
|
|
4730
|
+
doc: "The rendering context was lost. Rendering is paused and all graphics resources become invalid. Passed to the `render.set_listener` callback.",
|
|
4731
|
+
},
|
|
4732
|
+
],
|
|
4733
|
+
[
|
|
4734
|
+
"render.CONTEXT_EVENT_CONTEXT_RESTORED",
|
|
4735
|
+
{
|
|
4736
|
+
doc: "The rendering context was restored. Rendering is still paused and graphics resources are still invalid, but can be reloaded. Passed to the `render.set_listener` callback.",
|
|
4737
|
+
},
|
|
4738
|
+
],
|
|
4739
|
+
[
|
|
4740
|
+
"window.WINDOW_EVENT_ICONFIED",
|
|
4741
|
+
{
|
|
4742
|
+
doc: "Deprecated misspelling of `window.WINDOW_EVENT_ICONIFIED`, with the same value. The engine still registers it.",
|
|
4743
|
+
sameValueAs: "window.WINDOW_EVENT_ICONIFIED",
|
|
4744
|
+
},
|
|
4745
|
+
],
|
|
4746
|
+
]);
|
|
4747
|
+
|
|
4748
|
+
function withUndocumentedConstants(
|
|
4749
|
+
module: ApiModule,
|
|
4750
|
+
registered: ReadonlySet<string> | undefined,
|
|
4751
|
+
): ApiModule {
|
|
4752
|
+
const prefix = `${module.namespace}.`;
|
|
4753
|
+
const documented = new Set(module.constants.map((c) => c.name));
|
|
4754
|
+
const backfill: ApiConstant[] = [];
|
|
4755
|
+
for (const [fqn, { doc, sameValueAs }] of UNDOCUMENTED_CONSTANTS) {
|
|
4756
|
+
if (!fqn.startsWith(prefix) || fqn.slice(prefix.length).includes(".")) continue;
|
|
4757
|
+
if (documented.has(fqn)) continue;
|
|
4758
|
+
if (registered !== undefined && !registered.has(fqn)) continue;
|
|
4759
|
+
const aliasOf =
|
|
4760
|
+
sameValueAs !== undefined && documented.has(sameValueAs) ? sameValueAs : undefined;
|
|
4761
|
+
backfill.push({
|
|
4762
|
+
name: fqn,
|
|
4763
|
+
brief: doc,
|
|
4764
|
+
description: "",
|
|
4765
|
+
...(aliasOf === undefined ? {} : { aliasOf }),
|
|
4766
|
+
});
|
|
4767
|
+
}
|
|
4768
|
+
return backfill.length === 0
|
|
4769
|
+
? module
|
|
4770
|
+
: { ...module, constants: [...module.constants, ...backfill] };
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
// `constant` is the module's own element for `fqn`, absent for a constant another
|
|
4774
|
+
// module declares; an `ENUM` member's value type rides on it.
|
|
4775
|
+
function brandType(fqn: string, constant?: ApiConstant): string {
|
|
4776
|
+
// `undefined & { __brand }` collapses to `never`, so a nil constant takes no brand.
|
|
4777
|
+
if (NIL_CONSTANTS.has(fqn)) return "undefined";
|
|
4778
|
+
const base = constant?.valueType === "string" || STRING_CONSTANTS.has(fqn) ? "string" : "number";
|
|
4779
|
+
return `${base} & { readonly __brand: "${fqn}" }`;
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
// A declared constant's type. A nilable `ENUM` member admits `undefined` here
|
|
4783
|
+
// only: a slot naming it still takes the brand, as the value it is documented to
|
|
4784
|
+
// accept.
|
|
4785
|
+
function constantType(c: PreparedConstant): string {
|
|
4786
|
+
if (c.original.aliasOf !== undefined) return `typeof ${c.original.aliasOf}`;
|
|
4787
|
+
const brand = brandType(c.fqn, c.original);
|
|
4788
|
+
return c.original.nilable === true && brand !== "undefined" ? `${brand} | undefined` : brand;
|
|
2825
4789
|
}
|
|
2826
4790
|
|
|
2827
4791
|
function prepareVariable(v: ApiVariable, prefix: string): PreparedVariable | null {
|
|
@@ -2845,26 +4809,44 @@ function aliasName(name: string, aliases: { internal: string; public: string }[]
|
|
|
2845
4809
|
return internal;
|
|
2846
4810
|
}
|
|
2847
4811
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
4812
|
+
// A nested namespace named by a reserved word is emitted as `_<segment>`; its
|
|
4813
|
+
// parent re-exports it under the reserved name, sharing the alias a same-named
|
|
4814
|
+
// function already recorded.
|
|
4815
|
+
function aliasReservedSegments(
|
|
4816
|
+
segments: Iterable<string>,
|
|
4817
|
+
aliases: { internal: string; public: string }[],
|
|
4818
|
+
): void {
|
|
4819
|
+
for (const segment of segments) {
|
|
4820
|
+
if (!TS_RESERVED_NAMES.has(segment)) continue;
|
|
4821
|
+
if (aliases.some((alias) => alias.public === segment)) continue;
|
|
4822
|
+
aliases.push({ internal: `_${segment}`, public: segment });
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
// A variable whose ref-doc states its type only in prose, keyed by FQN. Each
|
|
4827
|
+
// entry fills a slot upstream left empty; `emit-dts.test.ts` reds once the
|
|
4828
|
+
// default target's ref-doc types it.
|
|
4829
|
+
export const VARIABLE_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
|
|
4830
|
+
// "A `string`, either:" followed by the four platform ids.
|
|
4831
|
+
["editor.platform", '"x86_64-win32" | "x86_64-macos" | "arm64-macos" | "x86_64-linux"'],
|
|
4832
|
+
]);
|
|
4833
|
+
|
|
4834
|
+
function emitVariable(prepared: PreparedVariable, name: string, mapType: DefoldTypeMapper): string {
|
|
2853
4835
|
const ts =
|
|
2854
4836
|
prepared.original.types.length > 0
|
|
2855
4837
|
? unionFromTokens(prepared.original.types, mapType)
|
|
2856
|
-
: "unknown";
|
|
4838
|
+
: (VARIABLE_TYPE_OVERRIDES.get(prepared.original.name) ?? "unknown");
|
|
2857
4839
|
return `const ${name}: ${ts};`;
|
|
2858
4840
|
}
|
|
2859
4841
|
|
|
2860
4842
|
function memberSignature(
|
|
2861
4843
|
prepared: PreparedFunction,
|
|
2862
4844
|
name: string,
|
|
2863
|
-
mapType:
|
|
4845
|
+
mapType: DefoldTypeMapper,
|
|
2864
4846
|
resolver: TableDocResolver,
|
|
2865
4847
|
constantTokens: ConstantSlotTokens,
|
|
2866
4848
|
urlParameters: UrlParameterTable,
|
|
2867
|
-
slotSink?: Record<string,
|
|
4849
|
+
slotSink?: Record<string, SlotType>,
|
|
2868
4850
|
): string {
|
|
2869
4851
|
const original = prepared.original.parameters;
|
|
2870
4852
|
const elementName = prepared.original.name;
|
|
@@ -2903,16 +4885,16 @@ function memberSignature(
|
|
|
2903
4885
|
if (slotSink !== undefined) {
|
|
2904
4886
|
for (const [i, p] of positional.entries()) {
|
|
2905
4887
|
const raw = original[i];
|
|
2906
|
-
if (raw) slotSink[slotKey("param", i, raw.name)] = p.
|
|
4888
|
+
if (raw) slotSink[slotKey("param", i, raw.name)] = { type: p.type, optional: p.optional };
|
|
2907
4889
|
}
|
|
2908
4890
|
for (const slot of rest?.slots ?? []) {
|
|
2909
|
-
slotSink[slotKey("param", slot.position, slot.name)] = slot.ts;
|
|
4891
|
+
slotSink[slotKey("param", slot.position, slot.name)] = { type: slot.ts, optional: false };
|
|
2910
4892
|
}
|
|
2911
4893
|
// A predicate renders `arg is T` in place of the mapped `boolean`, so its
|
|
2912
4894
|
// return slot has no text in the signature to stand on and is not reported.
|
|
2913
4895
|
if (!isPredicate) {
|
|
2914
4896
|
for (const slot of ret.slots) {
|
|
2915
|
-
slotSink[slotKey("return", slot.position, slot.name)] = slot.ts;
|
|
4897
|
+
slotSink[slotKey("return", slot.position, slot.name)] = { type: slot.ts, optional: false };
|
|
2916
4898
|
}
|
|
2917
4899
|
}
|
|
2918
4900
|
}
|
|
@@ -2925,11 +4907,11 @@ function memberSignature(
|
|
|
2925
4907
|
function emitFunction(
|
|
2926
4908
|
prepared: PreparedFunction,
|
|
2927
4909
|
name: string,
|
|
2928
|
-
mapType:
|
|
4910
|
+
mapType: DefoldTypeMapper,
|
|
2929
4911
|
resolver: TableDocResolver,
|
|
2930
4912
|
constantTokens: ConstantSlotTokens,
|
|
2931
4913
|
urlParameters: UrlParameterTable,
|
|
2932
|
-
slotSink?: Record<string,
|
|
4914
|
+
slotSink?: Record<string, SlotType>,
|
|
2933
4915
|
): string {
|
|
2934
4916
|
return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, slotSink)}`;
|
|
2935
4917
|
}
|
|
@@ -2939,11 +4921,11 @@ function emitFunction(
|
|
|
2939
4921
|
// member carries no `self` parameter.
|
|
2940
4922
|
function emitMethod(
|
|
2941
4923
|
prepared: PreparedFunction,
|
|
2942
|
-
mapType:
|
|
4924
|
+
mapType: DefoldTypeMapper,
|
|
2943
4925
|
resolver: TableDocResolver,
|
|
2944
4926
|
constantTokens: ConstantSlotTokens,
|
|
2945
4927
|
urlParameters: UrlParameterTable,
|
|
2946
|
-
slotSink?: Record<string,
|
|
4928
|
+
slotSink?: Record<string, SlotType>,
|
|
2947
4929
|
): string {
|
|
2948
4930
|
return memberSignature(
|
|
2949
4931
|
prepared,
|
|
@@ -2962,57 +4944,103 @@ function emitMethod(
|
|
|
2962
4944
|
// fallback applies to non-identifier names and a vararg drops its dots, matching
|
|
2963
4945
|
// `emitParameter` and `emitRestParameter`) so the tag resolves on hover; a
|
|
2964
4946
|
// parameter folded into a rest element union keeps its own tag, so the prose
|
|
2965
|
-
// documenting it survives. A single documented return becomes `@returns
|
|
2966
|
-
//
|
|
4947
|
+
// documenting it survives. A single documented return becomes `@returns`; a
|
|
4948
|
+
// multi-return function gains one only when a slot carries a classified index
|
|
4949
|
+
// (see `multiReturnDoc`). A return authored in `RETURN_TYPE_OVERRIDES` has no
|
|
4950
|
+
// ref-doc slot, so it gains `@returns` only for the base notes of its classified
|
|
4951
|
+
// fields (see `overrideReturnDoc`). Returns `[]` for a fully-undocumented
|
|
4952
|
+
// function, leaving its emission byte-identical.
|
|
2967
4953
|
function functionDocLines(
|
|
2968
4954
|
fn: ApiFunction,
|
|
2969
4955
|
translations: TranslationStore,
|
|
4956
|
+
baseNotes: IndexBaseNoteResolver,
|
|
2970
4957
|
indent: string = INDENT,
|
|
2971
4958
|
): string[] {
|
|
2972
4959
|
const params = fn.parameters.map((p, index) => ({
|
|
2973
4960
|
name: emittedParamName(p, index),
|
|
2974
|
-
doc:
|
|
4961
|
+
doc: appendNotes(
|
|
4962
|
+
translateProseFences(htmlToDocText(p.doc), fn.name, translations),
|
|
4963
|
+
baseNotes(fn.name, "param", p.name),
|
|
4964
|
+
),
|
|
2975
4965
|
}));
|
|
2976
4966
|
const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
|
|
4967
|
+
const notedReturns =
|
|
4968
|
+
multiReturnDoc(fn, translations, baseNotes) ?? overrideReturnDoc(fn, baseNotes);
|
|
2977
4969
|
const lua = htmlToCodeText(fn.examples ?? "");
|
|
2978
|
-
//
|
|
2979
|
-
//
|
|
2980
|
-
//
|
|
2981
|
-
//
|
|
2982
|
-
// absent translation) keeps the Lua fallback.
|
|
4970
|
+
// Each example resolves by its own segment hash, a single-example element's
|
|
4971
|
+
// one segment being the whole blob with no prose. The blob documents its
|
|
4972
|
+
// examples as TypeScript only when every segment has an authored body: a
|
|
4973
|
+
// partial resolve would drop the rest, so any miss keeps the Lua fallback.
|
|
2983
4974
|
const segments = splitExampleSources(fn.examples ?? "");
|
|
2984
|
-
const
|
|
2985
|
-
segments.length > 1
|
|
2986
|
-
? lookupExampleTranslations(
|
|
2987
|
-
translations,
|
|
2988
|
-
fn.name,
|
|
2989
|
-
segments.map((segment) => hashExampleSource(segment.code)),
|
|
2990
|
-
)
|
|
2991
|
-
: null;
|
|
2992
|
-
const ts = lua === "" ? null : lookupTranslation(translations, fn.name, hashExampleSource(lua));
|
|
4975
|
+
const resolved = lookupExampleSegments(translations, fn.name, segments);
|
|
2993
4976
|
const exampleParts: Pick<DocCommentParts, "examples"> =
|
|
2994
|
-
|
|
4977
|
+
resolved !== null
|
|
2995
4978
|
? {
|
|
2996
|
-
examples:
|
|
4979
|
+
examples: resolved.map(({ ts: text, prose }) => ({
|
|
2997
4980
|
text,
|
|
2998
4981
|
lang: "ts" as const,
|
|
2999
|
-
...(
|
|
4982
|
+
...(prose ? { prose } : {}),
|
|
3000
4983
|
})),
|
|
3001
4984
|
}
|
|
3002
|
-
:
|
|
3003
|
-
? { examples: [{ text:
|
|
3004
|
-
:
|
|
3005
|
-
? { examples: [{ text: lua, lang: "lua" }] }
|
|
3006
|
-
: {};
|
|
4985
|
+
: lua !== ""
|
|
4986
|
+
? { examples: [{ text: lua, lang: "lua" }] }
|
|
4987
|
+
: {};
|
|
3007
4988
|
const parts: DocCommentParts = {
|
|
3008
|
-
summary:
|
|
4989
|
+
summary: translateProseFences(
|
|
4990
|
+
htmlToDocText(summaryFor(fn.brief, fn.description)),
|
|
4991
|
+
fn.name,
|
|
4992
|
+
translations,
|
|
4993
|
+
),
|
|
3009
4994
|
params,
|
|
3010
|
-
...(onlyReturn
|
|
4995
|
+
...(onlyReturn
|
|
4996
|
+
? {
|
|
4997
|
+
returns: appendNotes(
|
|
4998
|
+
translateProseFences(htmlToDocText(onlyReturn.doc), fn.name, translations),
|
|
4999
|
+
baseNotes(fn.name, "return", onlyReturn.name),
|
|
5000
|
+
),
|
|
5001
|
+
}
|
|
5002
|
+
: notedReturns !== undefined
|
|
5003
|
+
? { returns: notedReturns }
|
|
5004
|
+
: {}),
|
|
3011
5005
|
...exampleParts,
|
|
3012
5006
|
};
|
|
3013
5007
|
return indentDocLines(parts, indent);
|
|
3014
5008
|
}
|
|
3015
5009
|
|
|
5010
|
+
// A multi-return function documents its tuple slot by slot, in tuple order, so
|
|
5011
|
+
// the hover can name each classified index's base. Every other multi-return
|
|
5012
|
+
// function keeps no `@returns`, leaving its emission byte-identical.
|
|
5013
|
+
function multiReturnDoc(
|
|
5014
|
+
fn: ApiFunction,
|
|
5015
|
+
translations: TranslationStore,
|
|
5016
|
+
baseNotes: IndexBaseNoteResolver,
|
|
5017
|
+
): string | undefined {
|
|
5018
|
+
if (fn.returnValues.length < 2) return undefined;
|
|
5019
|
+
const classified = fn.returnValues.some(
|
|
5020
|
+
(slot) => baseNotes(fn.name, "return", slot.name).length > 0,
|
|
5021
|
+
);
|
|
5022
|
+
if (!classified) return undefined;
|
|
5023
|
+
const bullets = fn.returnValues.map((slot) => {
|
|
5024
|
+
const doc = appendNotes(
|
|
5025
|
+
translateProseFences(htmlToDocText(slot.doc), fn.name, translations),
|
|
5026
|
+
baseNotes(fn.name, "return", slot.name),
|
|
5027
|
+
);
|
|
5028
|
+
const [first = "", ...rest] = doc.split("\n");
|
|
5029
|
+
return [
|
|
5030
|
+
`- \`${slot.name}\` — ${first}`,
|
|
5031
|
+
...rest.map((line) => (line === "" ? "" : ` ${line}`)),
|
|
5032
|
+
];
|
|
5033
|
+
});
|
|
5034
|
+
const names = fn.returnValues.map((slot) => slot.name).join(", ");
|
|
5035
|
+
return [`\`[${names}]\`:`, ...bullets.flat()].join("\n");
|
|
5036
|
+
}
|
|
5037
|
+
|
|
5038
|
+
function overrideReturnDoc(fn: ApiFunction, baseNotes: IndexBaseNoteResolver): string | undefined {
|
|
5039
|
+
if (fn.returnValues.length > 0 || !RETURN_TYPE_OVERRIDES.has(fn.name)) return undefined;
|
|
5040
|
+
const notes = baseNotes(fn.name, "return", OVERRIDE_RETURN_SLOT);
|
|
5041
|
+
return notes.length > 0 ? notes.join(" ") : undefined;
|
|
5042
|
+
}
|
|
5043
|
+
|
|
3016
5044
|
// Summary-only doc lines for a member that carries no params or returns
|
|
3017
5045
|
// (constants, variables, `properties` members). Reuses the same renderer and
|
|
3018
5046
|
// summary precedence as functions; returns `[]` for an undocumented member so
|
|
@@ -3022,11 +5050,11 @@ function summaryDocLines(brief: string, description: string, indent: string): st
|
|
|
3022
5050
|
return indentDocLines({ summary: htmlToDocText(summaryFor(brief, description)) }, indent);
|
|
3023
5051
|
}
|
|
3024
5052
|
|
|
3025
|
-
function namespaceDocLines(module: ApiModule): string[] {
|
|
5053
|
+
function namespaceDocLines(module: ApiModule, translations: TranslationStore): string[] {
|
|
3026
5054
|
const official = summaryFor(module.brief, module.description).trim();
|
|
3027
5055
|
const summary =
|
|
3028
5056
|
official.length > 0
|
|
3029
|
-
? htmlToDocText(official)
|
|
5057
|
+
? translateProseFences(htmlToDocText(official), module.namespace, translations)
|
|
3030
5058
|
: `(synthesized)\nDefold \`${module.namespace}\` API namespace.`;
|
|
3031
5059
|
return indentDocLines({ summary }, "");
|
|
3032
5060
|
}
|
|
@@ -3035,13 +5063,6 @@ function indentDocLines(parts: DocCommentParts, indent: string): string[] {
|
|
|
3035
5063
|
return renderDocComment(parts).map((line) => `${indent}${line}`);
|
|
3036
5064
|
}
|
|
3037
5065
|
|
|
3038
|
-
// Prefer the full `description`; fall back to the one-line `brief` when prose is
|
|
3039
|
-
// absent. Shared by every documented member kind so the summary source is
|
|
3040
|
-
// consistent across functions, constants, variables, and properties.
|
|
3041
|
-
export function summaryFor(brief: string, description: string): string {
|
|
3042
|
-
return description.trim() !== "" ? description : brief;
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
5066
|
export function isMarkedOptional(p: ApiParameter): boolean {
|
|
3046
5067
|
return p.isOptional || p.types.includes("nil");
|
|
3047
5068
|
}
|
|
@@ -3055,6 +5076,7 @@ export function isMarkedOptional(p: ApiParameter): boolean {
|
|
|
3055
5076
|
// second copy of the rule. The fidelity audit reads it too, to tell which slots
|
|
3056
5077
|
// the emitted surface still makes required.
|
|
3057
5078
|
export function isDocOptional(p: ApiParameter, elementName: string): boolean {
|
|
5079
|
+
if (requiredSlotCorrection(p, elementName)) return false;
|
|
3058
5080
|
if (isMarkedOptional(p)) return true;
|
|
3059
5081
|
return OPTIONAL_SLOT_CORRECTIONS.has(tableSlotKey(elementName, "param", p.name));
|
|
3060
5082
|
}
|
|
@@ -3082,7 +5104,7 @@ const SCENE_ADDRESS_ALIASES: Readonly<Record<string, string>> = {
|
|
|
3082
5104
|
// becomes the matching scene-derived alias. Wrapping `mapType` rather than
|
|
3083
5105
|
// rewriting the finished union leaves `mapSlotUnion`'s member ordering and
|
|
3084
5106
|
// de-duplication in charge, so only the one token moves.
|
|
3085
|
-
function addressMapType(mapType:
|
|
5107
|
+
function addressMapType(mapType: DefoldTypeMapper, alias: string): (t: string) => string {
|
|
3086
5108
|
return (token) => (token === "string" ? alias : mapType(token));
|
|
3087
5109
|
}
|
|
3088
5110
|
|
|
@@ -3091,30 +5113,84 @@ function addressMapType(mapType: (t: string) => string, alias: string): (t: stri
|
|
|
3091
5113
|
// type alone.
|
|
3092
5114
|
function parameterType(
|
|
3093
5115
|
p: ApiParameter,
|
|
3094
|
-
mapType:
|
|
5116
|
+
mapType: DefoldTypeMapper,
|
|
3095
5117
|
resolver: TableDocResolver,
|
|
3096
5118
|
constantTokens: ConstantSlotTokens,
|
|
3097
5119
|
elementName: string,
|
|
3098
5120
|
urlParameters: UrlParameterTable,
|
|
3099
5121
|
): string {
|
|
3100
5122
|
const concrete = p.types.filter((t) => t !== "nil");
|
|
5123
|
+
// The caller supplies a parameter, so a string-keyed table in it also takes an
|
|
5124
|
+
// object literal.
|
|
5125
|
+
const inputMapType = (token: string): string => mapType(token, "input");
|
|
3101
5126
|
// The table is keyed by the *raw* ref-doc parameter name, not the emitted
|
|
3102
5127
|
// `safeParamName` fallback.
|
|
3103
5128
|
const alias = SCENE_ADDRESS_ALIASES[classifyUrlParameter(urlParameters, elementName, p.name)];
|
|
3104
|
-
const slotMapType = alias === undefined ?
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
5129
|
+
const slotMapType = alias === undefined ? inputMapType : addressMapType(inputMapType, alias);
|
|
5130
|
+
const mapped =
|
|
5131
|
+
concrete.length > 0
|
|
5132
|
+
? mapSlotUnion(
|
|
5133
|
+
concrete,
|
|
5134
|
+
p.doc,
|
|
5135
|
+
slotMapType,
|
|
5136
|
+
true,
|
|
5137
|
+
resolver,
|
|
5138
|
+
constantTokens,
|
|
5139
|
+
elementName,
|
|
5140
|
+
"param",
|
|
5141
|
+
p.name,
|
|
5142
|
+
)
|
|
5143
|
+
: "unknown";
|
|
5144
|
+
const correction = PARAM_TYPE_CORRECTIONS.get(tableSlotKey(elementName, "param", p.name));
|
|
5145
|
+
if (correction === undefined || !pinMatches(p.types, correctionPins(correction))) return mapped;
|
|
5146
|
+
const kept =
|
|
5147
|
+
correction.removes === undefined ? mapped : withoutUnionMembers(mapped, correction.removes);
|
|
5148
|
+
if (correction.adds === undefined) return kept;
|
|
5149
|
+
if (kept === "") return correction.adds;
|
|
5150
|
+
const present = new Set(unionMembers(kept));
|
|
5151
|
+
const added = unionMembers(correction.adds).filter((member) => !present.has(member));
|
|
5152
|
+
return added.length === 0 ? kept : `${kept} | ${added.join(" | ")}`;
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
function isFunctionTypeText(ts: string): boolean {
|
|
5156
|
+
let depth = 0;
|
|
5157
|
+
for (let i = 0; i < ts.length - 1; i++) {
|
|
5158
|
+
const c = ts[i];
|
|
5159
|
+
if (c === "(" || c === "[" || c === "{" || c === "<") depth++;
|
|
5160
|
+
else if (c === ")" || c === "]" || c === "}" || (c === ">" && ts[i - 1] !== "=")) depth--;
|
|
5161
|
+
else if (c === "=" && ts[i + 1] === ">" && depth === 0) return true;
|
|
5162
|
+
}
|
|
5163
|
+
return false;
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5166
|
+
// The top-level members of a rendered union, leaving nested unions inside
|
|
5167
|
+
// brackets, braces or generics whole.
|
|
5168
|
+
function unionMembers(union: string): string[] {
|
|
5169
|
+
const members: string[] = [];
|
|
5170
|
+
let depth = 0;
|
|
5171
|
+
let start = 0;
|
|
5172
|
+
for (let i = 0; i < union.length; i++) {
|
|
5173
|
+
const c = union[i];
|
|
5174
|
+
if (c === "(" || c === "[" || c === "{" || c === "<") depth++;
|
|
5175
|
+
else if (c === ")" || c === "]" || c === "}" || (c === ">" && union[i - 1] !== "=")) depth--;
|
|
5176
|
+
else if (c === "|" && depth === 0) {
|
|
5177
|
+
members.push(union.slice(start, i).trim());
|
|
5178
|
+
start = i + 1;
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
members.push(union.slice(start).trim());
|
|
5182
|
+
return members;
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
// Drops whole top-level members from a rendered union. Called only once the
|
|
5186
|
+
// slot's pin matched, so a removal that finds nothing is a real defect.
|
|
5187
|
+
function withoutUnionMembers(union: string, removed: readonly string[]): string {
|
|
5188
|
+
const members = unionMembers(union);
|
|
5189
|
+
const kept = members.filter((member) => !removed.includes(member));
|
|
5190
|
+
if (kept.length === members.length) {
|
|
5191
|
+
throw new Error(`withoutUnionMembers: none of ${removed.join(", ")} is a member of ${union}`);
|
|
5192
|
+
}
|
|
5193
|
+
return kept.join(" | ");
|
|
3118
5194
|
}
|
|
3119
5195
|
|
|
3120
5196
|
const VARARG_PREFIX = "...";
|
|
@@ -3143,7 +5219,7 @@ function emittedParamName(p: ApiParameter, index: number): string {
|
|
|
3143
5219
|
function emitRestParameter(
|
|
3144
5220
|
params: readonly ApiParameter[],
|
|
3145
5221
|
varargIndex: number,
|
|
3146
|
-
mapType:
|
|
5222
|
+
mapType: DefoldTypeMapper,
|
|
3147
5223
|
resolver: TableDocResolver,
|
|
3148
5224
|
constantTokens: ConstantSlotTokens,
|
|
3149
5225
|
elementName: string,
|
|
@@ -3177,25 +5253,29 @@ function emitParameter(
|
|
|
3177
5253
|
p: ApiParameter,
|
|
3178
5254
|
index: number,
|
|
3179
5255
|
optional: boolean,
|
|
3180
|
-
mapType:
|
|
5256
|
+
mapType: DefoldTypeMapper,
|
|
3181
5257
|
resolver: TableDocResolver,
|
|
3182
5258
|
constantTokens: ConstantSlotTokens,
|
|
3183
5259
|
elementName: string,
|
|
3184
5260
|
urlParameters: UrlParameterTable,
|
|
3185
|
-
): { text: string;
|
|
5261
|
+
): { text: string; type: string; optional: boolean } {
|
|
3186
5262
|
const name = safeParamName(p.name, index);
|
|
3187
5263
|
const ts = parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters);
|
|
3188
5264
|
// An interior doc-optional param (a required param follows, so the trailing-`?`
|
|
3189
5265
|
// projection cannot mark it) keeps its optionality as `| undefined` — TSTL
|
|
3190
5266
|
// lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
|
|
3191
5267
|
// `?` form; required params are untouched.
|
|
3192
|
-
const
|
|
3193
|
-
|
|
5268
|
+
const nilRequired = requiredSlotCorrection(p, elementName)?.nil === true;
|
|
5269
|
+
const interiorOptional = !optional && (isDocOptional(p, elementName) || nilRequired);
|
|
5270
|
+
// `(a) => void | undefined` reads as a function returning `void | undefined`.
|
|
5271
|
+
const operand = interiorOptional && isFunctionTypeText(ts) ? `(${ts})` : ts;
|
|
5272
|
+
const type = `${operand}${interiorOptional ? " | undefined" : ""}`;
|
|
5273
|
+
return { text: `${name}${optional ? "?" : ""}: ${type}`, type, optional };
|
|
3194
5274
|
}
|
|
3195
5275
|
|
|
3196
5276
|
function emitReturn(
|
|
3197
5277
|
returnValues: ApiParameter[],
|
|
3198
|
-
mapType:
|
|
5278
|
+
mapType: DefoldTypeMapper,
|
|
3199
5279
|
resolver: TableDocResolver,
|
|
3200
5280
|
constantTokens: ConstantSlotTokens,
|
|
3201
5281
|
elementName: string,
|
|
@@ -3217,19 +5297,23 @@ function emitReturn(
|
|
|
3217
5297
|
position: index,
|
|
3218
5298
|
name: rv.name,
|
|
3219
5299
|
ts:
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
5300
|
+
correction?.slot !== undefined &&
|
|
5301
|
+
correction.slot === rv.name &&
|
|
5302
|
+
pinMatches(rv.types, correctionPins(correction))
|
|
5303
|
+
? correction.ts
|
|
5304
|
+
: rv.types.length > 0
|
|
5305
|
+
? mapSlotUnion(
|
|
5306
|
+
rv.types,
|
|
5307
|
+
rv.doc,
|
|
5308
|
+
mapType,
|
|
5309
|
+
false,
|
|
5310
|
+
resolver,
|
|
5311
|
+
constantTokens,
|
|
5312
|
+
elementName,
|
|
5313
|
+
"return",
|
|
5314
|
+
rv.name,
|
|
5315
|
+
)
|
|
5316
|
+
: "unknown",
|
|
3233
5317
|
}));
|
|
3234
5318
|
return {
|
|
3235
5319
|
type: `LuaMultiReturn<[${emitted.map((slot) => slot.ts).join(", ")}]>`,
|
|
@@ -3239,7 +5323,11 @@ function emitReturn(
|
|
|
3239
5323
|
}
|
|
3240
5324
|
const first = returnValues[0];
|
|
3241
5325
|
if (!first) return { type: "void", trailing: "", slots: [] };
|
|
3242
|
-
if (
|
|
5326
|
+
if (
|
|
5327
|
+
correction !== undefined &&
|
|
5328
|
+
correction.slot === undefined &&
|
|
5329
|
+
pinMatches(first.types, correctionPins(correction))
|
|
5330
|
+
) {
|
|
3243
5331
|
return {
|
|
3244
5332
|
type: correction.ts,
|
|
3245
5333
|
trailing: "",
|
|
@@ -3260,23 +5348,41 @@ function emitReturn(
|
|
|
3260
5348
|
first.name,
|
|
3261
5349
|
)
|
|
3262
5350
|
: "unknown";
|
|
5351
|
+
// A LuaLS vararg return (`...`) is every value the call returns, however many.
|
|
5352
|
+
if (first.name === "...") {
|
|
5353
|
+
const type = `LuaMultiReturn<${needsArrayParens(ts) ? `(${ts})` : ts}[]>`;
|
|
5354
|
+
return { type, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
|
|
5355
|
+
}
|
|
3263
5356
|
return { type: ts, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
|
|
3264
5357
|
}
|
|
3265
5358
|
|
|
3266
|
-
|
|
5359
|
+
// The mark upstream opens a property's description with when the engine refuses
|
|
5360
|
+
// to write it: `go.set` raises "Unable to set the property ... since it is read
|
|
5361
|
+
// only".
|
|
5362
|
+
const READ_ONLY_MARK = /^\s*<span class="mark">READ ONLY<\/span>/;
|
|
5363
|
+
|
|
5364
|
+
function emitPropertyMembers(
|
|
3267
5365
|
p: ApiProperty,
|
|
3268
|
-
mapType:
|
|
5366
|
+
mapType: DefoldTypeMapper,
|
|
3269
5367
|
namespace: string,
|
|
3270
|
-
): string {
|
|
3271
|
-
const
|
|
5368
|
+
): readonly string[] {
|
|
5369
|
+
const keyRange = PROPERTY_KEY_RANGES.get(`${namespace}.${p.name}`);
|
|
5370
|
+
const keys: string[] = [];
|
|
5371
|
+
if (keyRange !== undefined) {
|
|
5372
|
+
for (let unit = keyRange.first; unit <= keyRange.last; unit++)
|
|
5373
|
+
keys.push(`${keyRange.prefix}${unit}`);
|
|
5374
|
+
} else {
|
|
5375
|
+
keys.push(TS_IDENTIFIER.test(p.name) ? p.name : JSON.stringify(p.name));
|
|
5376
|
+
}
|
|
3272
5377
|
const correction = PROPERTY_TYPE_CORRECTIONS.get(`${namespace}.${p.name}`);
|
|
3273
5378
|
const ts =
|
|
3274
|
-
correction !== undefined
|
|
5379
|
+
correction !== undefined && pinMatches(p.types, propertyCorrectionPins(correction))
|
|
3275
5380
|
? correction.ts
|
|
3276
5381
|
: p.types.length > 0
|
|
3277
5382
|
? unionFromTokens(p.types, mapType)
|
|
3278
5383
|
: "unknown";
|
|
3279
|
-
|
|
5384
|
+
const modifier = READ_ONLY_MARK.test(p.description) ? "readonly " : "";
|
|
5385
|
+
return keys.map((key) => `${modifier}${key}: ${ts};`);
|
|
3280
5386
|
}
|
|
3281
5387
|
|
|
3282
5388
|
// Like `unionFromTokens`, but a `table` token whose slot doc carries a parseable
|
|
@@ -3289,7 +5395,7 @@ function emitPropertyMember(
|
|
|
3289
5395
|
function mapSlotUnion(
|
|
3290
5396
|
types: readonly string[],
|
|
3291
5397
|
doc: string,
|
|
3292
|
-
mapType:
|
|
5398
|
+
mapType: DefoldTypeMapper,
|
|
3293
5399
|
optionalFields: boolean,
|
|
3294
5400
|
resolver: TableDocResolver,
|
|
3295
5401
|
constantTokens: ConstantSlotTokens,
|
|
@@ -3313,11 +5419,12 @@ function mapSlotUnion(
|
|
|
3313
5419
|
}
|
|
3314
5420
|
}
|
|
3315
5421
|
let ts: string;
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
5422
|
+
const slotCuration =
|
|
5423
|
+
slotKind !== undefined && slotName !== undefined
|
|
5424
|
+
? TABLE_SLOT_CURATIONS.get(tableSlotKey(elementName, slotKind, slotName))
|
|
5425
|
+
: undefined;
|
|
5426
|
+
if (token === "table" || slotCuration?.retypedUpstream?.includes(token) === true) {
|
|
5427
|
+
const curation = slotCuration;
|
|
3321
5428
|
const mapping =
|
|
3322
5429
|
curation?.kind === "mapping" ? curation : MAPPING_TABLE_SLOTS.get(elementName);
|
|
3323
5430
|
const element =
|
|
@@ -3341,15 +5448,33 @@ function mapSlotUnion(
|
|
|
3341
5448
|
// A mapping key may be a `A | B` union token too (render.clear's three
|
|
3342
5449
|
// documented graphics.BUFFER_TYPE_* constants); split it the same way
|
|
3343
5450
|
// the value above is split. A single-token key (no `|`) is unaffected.
|
|
3344
|
-
const
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
5451
|
+
const keyTokens = mapping.key.split("|").map((token) => token.trim());
|
|
5452
|
+
if (slotKind === "param" && keyTokens.includes("string")) {
|
|
5453
|
+
// A caller builds a string-keyed parameter table as an object literal,
|
|
5454
|
+
// which `LuaMap` rejects; the `Record` arm admits it and TSTL lowers
|
|
5455
|
+
// both to the same Lua table. The `Record` arm carries the string keys,
|
|
5456
|
+
// so `LuaMap` keeps only the other key tokens: `LuaMap`'s key is
|
|
5457
|
+
// invariant, and a `LuaMap<Hash | string, V>` would reject the
|
|
5458
|
+
// `LuaMap<Hash, V>` a caller builds. A string-only key keeps its
|
|
5459
|
+
// `LuaMap` arm so an existing `LuaMap` caller stays valid.
|
|
5460
|
+
const otherKeys = keyTokens.filter((token) => token !== "string");
|
|
5461
|
+
const key = unionFromTokens(otherKeys.length > 0 ? otherKeys : keyTokens, mapType);
|
|
5462
|
+
ts = `LuaMap<${key}, ${value}> | Record<string, ${value}>`;
|
|
5463
|
+
} else {
|
|
5464
|
+
ts = `LuaMap<${unionFromTokens(keyTokens, mapType)}, ${value}>`;
|
|
5465
|
+
}
|
|
3349
5466
|
} else if (element !== undefined) {
|
|
3350
5467
|
ts = arrayTypeFromTokens(element, mapType);
|
|
5468
|
+
} else if (curation?.kind === "verbatim") {
|
|
5469
|
+
ts = curation.ts;
|
|
3351
5470
|
} else if (curation?.kind === "object" || curation?.kind === "array-object") {
|
|
3352
|
-
const
|
|
5471
|
+
const fields = applyRequiredFieldCorrections(
|
|
5472
|
+
elementName,
|
|
5473
|
+
slotKind,
|
|
5474
|
+
slotName,
|
|
5475
|
+
curation.fields,
|
|
5476
|
+
);
|
|
5477
|
+
const object = inlineTableType(fields, mapType, optionalFields);
|
|
3353
5478
|
ts = curation.kind === "array-object" ? `${object}[]` : object;
|
|
3354
5479
|
} else if (curation?.kind === "keyed-object") {
|
|
3355
5480
|
// The doc's `<dl>` describes the value shape; parse it and re-key by an
|
|
@@ -3375,7 +5500,13 @@ function mapSlotUnion(
|
|
|
3375
5500
|
const added = applyFieldAdditions(elementName, slotKind, slotName, parsed);
|
|
3376
5501
|
const nested = applyNestedFieldCurations(elementName, slotKind, slotName, added);
|
|
3377
5502
|
const typed = applyFieldTypeOverrides(elementName, slotKind, slotName, nested);
|
|
3378
|
-
const
|
|
5503
|
+
const required = applyRequiredFieldCorrections(
|
|
5504
|
+
elementName,
|
|
5505
|
+
slotKind,
|
|
5506
|
+
slotName,
|
|
5507
|
+
applyFieldOptionalityCorrections(elementName, slotKind, slotName, typed),
|
|
5508
|
+
);
|
|
5509
|
+
const fields = applyListFieldCorrections(elementName, slotKind, slotName, required);
|
|
3379
5510
|
const object = inlineTableType(fields, mapType, optionalFields);
|
|
3380
5511
|
ts = isSlotLevelList(doc) ? `${object}[]` : object;
|
|
3381
5512
|
} else {
|
|
@@ -3430,18 +5561,21 @@ export function applyNestedFieldCurations(
|
|
|
3430
5561
|
): TableField[] {
|
|
3431
5562
|
if (slotKind === undefined || slotName === undefined) return [...fields];
|
|
3432
5563
|
return fields.map((field) => {
|
|
3433
|
-
if (field.fields !== undefined) return field;
|
|
3434
5564
|
const curated = NESTED_FIELD_CURATIONS.get(
|
|
3435
5565
|
nestedFieldKey(elementName, slotKind, slotName, field.name),
|
|
3436
5566
|
);
|
|
3437
5567
|
if (curated === undefined) return field;
|
|
3438
|
-
|
|
5568
|
+
// A parsed member list keeps its members; the curation supplies the ones
|
|
5569
|
+
// upstream flattened to the slot's top level.
|
|
5570
|
+
const parsed = field.fields ?? [];
|
|
5571
|
+
const missing = curated.filter((member) => !parsed.some((p) => p.name === member.name));
|
|
5572
|
+
return { ...field, fields: [...parsed, ...missing], isList: true };
|
|
3439
5573
|
});
|
|
3440
5574
|
}
|
|
3441
5575
|
|
|
3442
5576
|
function arrayTypeFromTokens(
|
|
3443
5577
|
element: string | readonly string[],
|
|
3444
|
-
mapType:
|
|
5578
|
+
mapType: DefoldTypeMapper,
|
|
3445
5579
|
): string {
|
|
3446
5580
|
const tokens = typeof element === "string" ? [element] : element;
|
|
3447
5581
|
return tokens.length > 1
|
|
@@ -3451,7 +5585,7 @@ function arrayTypeFromTokens(
|
|
|
3451
5585
|
|
|
3452
5586
|
export function inlineTableType(
|
|
3453
5587
|
fields: readonly TableField[],
|
|
3454
|
-
mapType:
|
|
5588
|
+
mapType: DefoldTypeMapper,
|
|
3455
5589
|
optionalFields: boolean,
|
|
3456
5590
|
): string {
|
|
3457
5591
|
const members = fields.map((field) => {
|
|
@@ -3469,7 +5603,7 @@ export function inlineTableType(
|
|
|
3469
5603
|
field.tsType !== undefined
|
|
3470
5604
|
? field.tsType
|
|
3471
5605
|
: field.fields !== undefined
|
|
3472
|
-
?
|
|
5606
|
+
? nestedFieldType(field, inlineTableType(field.fields, mapType, optionalFields))
|
|
3473
5607
|
: field.numberList === true
|
|
3474
5608
|
? "number[]"
|
|
3475
5609
|
: field.types.length > 0
|
|
@@ -3478,12 +5612,21 @@ export function inlineTableType(
|
|
|
3478
5612
|
// A field is optional when the whole slot is param-side (optionalFields) or
|
|
3479
5613
|
// when the curation marks that individual field optional (a return-side
|
|
3480
5614
|
// variant field present only for some shape kinds, e.g. a b2d shape table).
|
|
3481
|
-
|
|
5615
|
+
// A param-side field the engine requires stays required.
|
|
5616
|
+
const optional = (optionalFields && field.required !== true) || field.optional === true;
|
|
5617
|
+
return `${key}${optional ? "?" : ""}: ${ts}`;
|
|
3482
5618
|
});
|
|
3483
5619
|
return `{ ${members.join("; ")} }`;
|
|
3484
5620
|
}
|
|
3485
5621
|
|
|
3486
|
-
function
|
|
5622
|
+
function nestedFieldType(field: TableField, object: string): string {
|
|
5623
|
+
const element = field.alternatives === undefined ? object : `${object} & (${field.alternatives})`;
|
|
5624
|
+
if (field.isList !== true) return element;
|
|
5625
|
+
const rest = field.alternatives === undefined ? element : `(${element})`;
|
|
5626
|
+
return field.nonEmpty === true ? `[${element}, ...${rest}[]]` : `${rest}[]`;
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
function unionFromTokens(tokens: readonly string[], mapType: DefoldTypeMapper): string {
|
|
3487
5630
|
const mapped: string[] = [];
|
|
3488
5631
|
const seen = new Set<string>();
|
|
3489
5632
|
for (const token of tokens) {
|
|
@@ -3495,7 +5638,7 @@ function unionFromTokens(tokens: readonly string[], mapType: (t: string) => stri
|
|
|
3495
5638
|
return joinUnionArms(mapped);
|
|
3496
5639
|
}
|
|
3497
5640
|
|
|
3498
|
-
const BRAND_ARM = /^number & \{ readonly __brand: "([^"]+)" \}$/;
|
|
5641
|
+
const BRAND_ARM = /^(?:number|string) & \{ readonly __brand: "([^"]+)" \}$/;
|
|
3499
5642
|
|
|
3500
5643
|
// Joins already-mapped arms, collapsing the branded ones to their alias when
|
|
3501
5644
|
// they are exactly a `CONSTANT_UNION_ALIASES` member set. The alias takes the
|
|
@@ -3524,20 +5667,70 @@ function joinUnionArms(mapped: readonly string[]): string {
|
|
|
3524
5667
|
return out.join(" | ");
|
|
3525
5668
|
}
|
|
3526
5669
|
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
5670
|
+
const BUILTIN_MESSAGE_NAMES: ReadonlySet<string> = new Set(
|
|
5671
|
+
messagesDoc.messages.map((message) => message.name),
|
|
5672
|
+
);
|
|
5673
|
+
const MESSAGE_REFERENCE = /^message\.[a-z_]+\.([a-z_]+)$/;
|
|
5674
|
+
|
|
5675
|
+
// The leaf vocabulary the LuaLS grammar resolves an engine type expression
|
|
5676
|
+
// against. `message.<ns>.<name>` names a documented message payload, typed only
|
|
5677
|
+
// when `<name>` is one `BuiltinMessages` declares.
|
|
5678
|
+
function resolveDefoldLeaf(leaf: string): string | undefined {
|
|
5679
|
+
if (leaf === "nil") return "undefined";
|
|
5680
|
+
if (Object.hasOwn(DEFOLD_TYPE_MAP, leaf)) return DEFOLD_TYPE_MAP[leaf];
|
|
5681
|
+
const message = MESSAGE_REFERENCE.exec(leaf);
|
|
5682
|
+
const name = message?.[1];
|
|
5683
|
+
if (name !== undefined && BUILTIN_MESSAGE_NAMES.has(name)) return `BuiltinMessages["${name}"]`;
|
|
5684
|
+
return undefined;
|
|
3533
5685
|
}
|
|
3534
5686
|
|
|
3535
|
-
|
|
5687
|
+
// Whole-token lookup and callback recovery run first, so the grammar sees only
|
|
5688
|
+
// what they miss; every pre-1.13.2 token resolves before it is reached.
|
|
5689
|
+
function wholeTokenMapping(token: string): string | undefined {
|
|
3536
5690
|
if (Object.hasOwn(DEFOLD_TYPE_MAP, token)) {
|
|
3537
5691
|
const mapped = DEFOLD_TYPE_MAP[token];
|
|
3538
5692
|
if (typeof mapped === "string") return mapped;
|
|
3539
5693
|
}
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
5694
|
+
return recoverCallbackSignature(token) ?? undefined;
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
/** The leaves of `token` that resolve to nothing; empty when the whole token maps. */
|
|
5698
|
+
export function unknownDefoldTypeLeaves(token: string): string[] {
|
|
5699
|
+
if (token === "nil" || wholeTokenMapping(token) !== undefined) return [];
|
|
5700
|
+
if (token.trim() === "") return [token];
|
|
5701
|
+
return mapLualsExpression(token, resolveDefoldLeaf).unknowns;
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
export function isKnownDefoldTypeToken(token: string): boolean {
|
|
5705
|
+
return unknownDefoldTypeLeaves(token).length === 0;
|
|
5706
|
+
}
|
|
5707
|
+
|
|
5708
|
+
export function defaultMapType(token: string, position?: LualsPosition): string {
|
|
5709
|
+
return wholeTokenMapping(token) ?? mapLualsExpression(token, resolveDefoldLeaf, position).ts;
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
/**
|
|
5713
|
+
* The engine mapper extended with `leaves`, resolved ahead of the engine
|
|
5714
|
+
* vocabulary, and `declared` (Lua name -> TS path), consulted after it. A whole
|
|
5715
|
+
* token checks `leaves`, the engine's whole-token mapping, then `declared`; a
|
|
5716
|
+
* leaf inside a composite checks `leaves`, the engine leaves, then `declared`.
|
|
5717
|
+
* So a surface's handle brand (`editor.message`) outranks any declared path.
|
|
5718
|
+
*/
|
|
5719
|
+
export function mapTypeWithVocabulary(vocabulary: {
|
|
5720
|
+
leaves?: Readonly<Record<string, string>> | undefined;
|
|
5721
|
+
declared?: ReadonlyMap<string, string> | undefined;
|
|
5722
|
+
}): DefoldTypeMapper {
|
|
5723
|
+
const leaves = vocabulary.leaves ?? {};
|
|
5724
|
+
const declared = vocabulary.declared ?? new Map<string, string>();
|
|
5725
|
+
if (Object.keys(leaves).length === 0 && declared.size === 0) return defaultMapType;
|
|
5726
|
+
const resolveLeaf = (leaf: string): string | undefined =>
|
|
5727
|
+
Object.hasOwn(leaves, leaf) ? leaves[leaf] : (resolveDefoldLeaf(leaf) ?? declared.get(leaf));
|
|
5728
|
+
return (token, position) => {
|
|
5729
|
+
if (Object.hasOwn(leaves, token)) return leaves[token] as string;
|
|
5730
|
+
return (
|
|
5731
|
+
wholeTokenMapping(token) ??
|
|
5732
|
+
declared.get(token) ??
|
|
5733
|
+
mapLualsExpression(token, resolveLeaf, position).ts
|
|
5734
|
+
);
|
|
5735
|
+
};
|
|
3543
5736
|
}
|