@defold-typescript/library-types 0.20.7 → 0.20.8

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Runs the LuaLS-to-TS mapper (`map-luals-types.ts`) over a whole parsed
3
+ * `LibraryModel` and tallies how well the library's types survive the trip. It
4
+ * writes nothing and reads nothing — a pure function over the model — so a
5
+ * committed per-library report is a byte-stable regression guard.
6
+ *
7
+ * The mapper's loud-fail on an unmapped `vmath.*` token propagates: a core-type
8
+ * gap reds the report rather than hiding behind a coverage number. Every other
9
+ * unresolved reference is a recorded `unknown`, surfaced here as a count and a
10
+ * sorted-unique token list so the gap is visible instead of silent.
11
+ */
12
+
13
+ import { type MapContext, mapLualsType } from "./map-luals-types";
14
+ import type { LibraryModel } from "./parse-luals";
15
+
16
+ export interface FidelityReport {
17
+ namespace: string;
18
+ totalMembers: number;
19
+ totalTypeTokens: number;
20
+ unknownFallbacks: number;
21
+ unknownTokens: string[];
22
+ undocumentedMembers: number;
23
+ coverage: number;
24
+ }
25
+
26
+ function round3(value: number): number {
27
+ return Math.round(value * 1000) / 1000;
28
+ }
29
+
30
+ /**
31
+ * Build the fidelity report for one namespace. `knownNames` is drawn from the
32
+ * model's own interface and alias names so a reference to a sibling library type
33
+ * resolves rather than falling to `unknown`. Every field, every param and return
34
+ * of every method and module function, and every alias expression is mapped;
35
+ * `undocumentedMembers` counts fields/methods/moduleFunctions whose doc or brief
36
+ * is empty. Deterministic; no I/O.
37
+ */
38
+ export function buildFidelityReport(
39
+ namespace: string,
40
+ model: LibraryModel,
41
+ typeRenames: Record<string, string>,
42
+ ): FidelityReport {
43
+ const knownNames = new Set<string>();
44
+ for (const iface of model.interfaces) knownNames.add(iface.name);
45
+ for (const alias of model.aliases) knownNames.add(alias.name);
46
+ const ctx: MapContext = { knownNames, typeRenames };
47
+
48
+ let totalMembers = 0;
49
+ let totalTypeTokens = 0;
50
+ let unknownFallbacks = 0;
51
+ let undocumentedMembers = 0;
52
+ const unknownTokens = new Set<string>();
53
+
54
+ const mapTokens = (tokens: string[]): void => {
55
+ for (const token of tokens) {
56
+ totalTypeTokens++;
57
+ const { unknowns } = mapLualsType(token, ctx);
58
+ unknownFallbacks += unknowns.length;
59
+ for (const u of unknowns) unknownTokens.add(u);
60
+ }
61
+ };
62
+
63
+ const undocumented = (doc: string): boolean => doc.trim() === "";
64
+
65
+ for (const iface of model.interfaces) {
66
+ for (const field of iface.fields) {
67
+ totalMembers++;
68
+ if (undocumented(field.doc)) undocumentedMembers++;
69
+ mapTokens(field.types);
70
+ }
71
+ for (const method of iface.methods) {
72
+ totalMembers++;
73
+ if (undocumented(method.brief)) undocumentedMembers++;
74
+ for (const param of method.params) mapTokens(param.types);
75
+ for (const ret of method.returns) mapTokens(ret.types);
76
+ }
77
+ }
78
+
79
+ for (const fn of model.moduleFunctions) {
80
+ totalMembers++;
81
+ if (undocumented(fn.brief)) undocumentedMembers++;
82
+ for (const param of fn.params) mapTokens(param.types);
83
+ for (const ret of fn.returns) mapTokens(ret.types);
84
+ }
85
+
86
+ for (const alias of model.aliases) mapTokens(alias.types);
87
+
88
+ const coverage =
89
+ totalTypeTokens === 0
90
+ ? 1
91
+ : round3(Math.max(0, Math.min(1, (totalTypeTokens - unknownFallbacks) / totalTypeTokens)));
92
+
93
+ return {
94
+ namespace,
95
+ totalMembers,
96
+ totalTypeTokens,
97
+ unknownFallbacks,
98
+ unknownTokens: [...unknownTokens].sort(),
99
+ undocumentedMembers,
100
+ coverage,
101
+ };
102
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Turns the raw LuaLS type-expression tokens `parse-luals.ts` preserves verbatim
3
+ * (`integer`, `string?`, `fun(self):number`, `table<K,V>`, `druid.component`,
4
+ * `vmath.vector3`) into TypeScript type strings. Pure and deterministic: mapping a
5
+ * token has no I/O and depends only on the token and the supplied `MapContext`.
6
+ *
7
+ * It mirrors two disciplines from the ts-defold front-end
8
+ * (`sync-library-types.ts`): a `vmath.*`-namespaced token with no rename is a hard
9
+ * error (a missing core mapping must surface, never lower to a silent `any`); any
10
+ * other unresolved reference lowers to `unknown` and is recorded so a fidelity
11
+ * report can show the gap. Scope is mapping only — no declaration text, no
12
+ * identifier sanitization; a known class reference resolves to its model name
13
+ * verbatim and the emitter sanitizes it later.
14
+ */
15
+
16
+ import { CORE_TYPE_RENAMES } from "./sync-library-types";
17
+
18
+ export interface MapContext {
19
+ knownNames: ReadonlySet<string>;
20
+ typeRenames: Readonly<Record<string, string>>;
21
+ }
22
+
23
+ export interface MapResult {
24
+ ts: string;
25
+ unknowns: string[];
26
+ }
27
+
28
+ const SCALARS: Readonly<Record<string, string>> = {
29
+ integer: "number",
30
+ number: "number",
31
+ string: "string",
32
+ boolean: "boolean",
33
+ nil: "undefined",
34
+ any: "unknown",
35
+ };
36
+
37
+ /**
38
+ * Split `s` on every top-level occurrence of the single-character `sep`, honoring
39
+ * bracket depth and double-quoted string literals so a separator nested inside
40
+ * `<...>`, `(...)`, `[...]`, `{...}`, or a `"..."` literal does not split.
41
+ */
42
+ function splitTopLevel(s: string, sep: string): string[] {
43
+ const parts: string[] = [];
44
+ let depth = 0;
45
+ let inQuote = false;
46
+ let start = 0;
47
+ for (let i = 0; i < s.length; i++) {
48
+ const c = s[i];
49
+ if (inQuote) {
50
+ if (c === '"') inQuote = false;
51
+ continue;
52
+ }
53
+ if (c === '"') inQuote = true;
54
+ else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
55
+ else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
56
+ else if (depth === 0 && c === sep) {
57
+ parts.push(s.slice(start, i));
58
+ start = i + 1;
59
+ }
60
+ }
61
+ parts.push(s.slice(start));
62
+ return parts;
63
+ }
64
+
65
+ /** Index of the matching close bracket for the opener at `open`, or -1 if unbalanced. */
66
+ function matchBracket(s: string, open: number): number {
67
+ const closers: Record<string, string> = { "<": ">", "(": ")", "[": "]", "{": "}" };
68
+ const want = closers[s[open] as string];
69
+ let depth = 0;
70
+ let inQuote = false;
71
+ for (let i = open; i < s.length; i++) {
72
+ const c = s[i];
73
+ if (inQuote) {
74
+ if (c === '"') inQuote = false;
75
+ continue;
76
+ }
77
+ if (c === '"') inQuote = true;
78
+ else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
79
+ else if (c === ">" || c === ")" || c === "]" || c === "}") {
80
+ depth--;
81
+ if (depth === 0) return c === want ? i : -1;
82
+ }
83
+ }
84
+ return -1;
85
+ }
86
+
87
+ /** True when a top-level `=>` (an arrow function type) appears in a mapped result. */
88
+ function hasTopLevelArrow(tsExpr: string): boolean {
89
+ let depth = 0;
90
+ for (let i = 0; i + 1 < tsExpr.length; i++) {
91
+ const c = tsExpr[i];
92
+ if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
93
+ else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
94
+ else if (depth === 0 && c === "=" && tsExpr[i + 1] === ">") return true;
95
+ }
96
+ return false;
97
+ }
98
+
99
+ /** A union member needs parentheses when it is itself a function type. */
100
+ function wrapForUnion(tsExpr: string): string {
101
+ return hasTopLevelArrow(tsExpr) ? `(${tsExpr})` : tsExpr;
102
+ }
103
+
104
+ /** An array element needs parentheses when it is a union, a function, or an object. */
105
+ function needsArrayParens(tsExpr: string): boolean {
106
+ return (
107
+ splitTopLevel(tsExpr, "|").length > 1 || hasTopLevelArrow(tsExpr) || tsExpr.startsWith("{")
108
+ );
109
+ }
110
+
111
+ function mapFunction(token: string, ctx: MapContext, unknowns: string[]): string {
112
+ const open = token.indexOf("(");
113
+ const close = matchBracket(token, open);
114
+ const paramsStr = token.slice(open + 1, close).trim();
115
+ const afterClose = token.slice(close + 1).trim();
116
+
117
+ const params = paramsStr === "" ? [] : splitTopLevel(paramsStr, ",");
118
+ const paramList = params
119
+ .map((raw) => raw.trim())
120
+ .map((part) => {
121
+ if (part.startsWith("...")) {
122
+ const after = part.slice(3).trim();
123
+ let element: string;
124
+ if (after.startsWith(":")) {
125
+ element = mapToken(after.slice(1).trim(), ctx, unknowns);
126
+ } else {
127
+ element = "unknown";
128
+ unknowns.push("...");
129
+ }
130
+ return `...args: ${needsArrayParens(element) ? `(${element})[]` : `${element}[]`}`;
131
+ }
132
+ const colon = splitTopLevel(part, ":");
133
+ if (colon.length < 2) {
134
+ // Untyped param (`self`, `_`, `ctx`): a recorded gap, not a silent `any`.
135
+ unknowns.push(part);
136
+ return `${part}: unknown`;
137
+ }
138
+ const name = colon[0]?.trim() ?? "";
139
+ const typeExpr = colon.slice(1).join(":").trim();
140
+ const mapped = mapToken(typeExpr, ctx, unknowns);
141
+ return `${name}: ${mapped}`;
142
+ })
143
+ .join(", ");
144
+
145
+ let ret = "void";
146
+ if (afterClose.startsWith(":")) {
147
+ const retStr = afterClose.slice(1).trim();
148
+ const retTokens = retStr === "" ? [] : splitTopLevel(retStr, ",").map((r) => r.trim());
149
+ if (retTokens.length === 1) {
150
+ ret = mapToken(retTokens[0] as string, ctx, unknowns);
151
+ } else if (retTokens.length > 1) {
152
+ const inner = retTokens.map((r) => mapToken(r, ctx, unknowns)).join(", ");
153
+ ret = `LuaMultiReturn<[${inner}]>`;
154
+ }
155
+ }
156
+ return `(${paramList}) => ${ret}`;
157
+ }
158
+
159
+ function mapObject(token: string, ctx: MapContext, unknowns: string[]): string {
160
+ const inner = token.slice(1, -1).trim();
161
+ if (inner === "") return "{}";
162
+ const entries = splitTopLevel(inner, ",")
163
+ .map((raw) => raw.trim())
164
+ .filter((part) => part.length > 0)
165
+ .map((part) => {
166
+ const colon = splitTopLevel(part, ":");
167
+ const key = colon[0]?.trim() ?? "";
168
+ const typeExpr = colon.slice(1).join(":").trim();
169
+ return `${key}: ${mapToken(typeExpr, ctx, unknowns)}`;
170
+ });
171
+ return `{ ${entries.join("; ")} }`;
172
+ }
173
+
174
+ function mapToken(raw: string, ctx: MapContext, unknowns: string[]): string {
175
+ let token = raw.trim();
176
+
177
+ // Strip a redundant pair of outer parentheses (LuaLS grouping) so `(a | b)[]`
178
+ // reaches the union handler rather than falling through to a reference lookup.
179
+ while (token.startsWith("(") && matchBracket(token, 0) === token.length - 1) {
180
+ token = token.slice(1, -1).trim();
181
+ }
182
+
183
+ if (token === "") return "unknown";
184
+
185
+ // Optional suffix.
186
+ if (token.length > 1 && token.endsWith("?")) {
187
+ const base = mapToken(token.slice(0, -1), ctx, unknowns);
188
+ const members = splitTopLevel(base, "|").map((m) => m.trim());
189
+ return members.includes("undefined") ? base : `${base} | undefined`;
190
+ }
191
+
192
+ // A `fun(...)` whose return follows the `)` keeps its return-type `|` inside the
193
+ // function; splitting the union first would cut `fun(): a|b` into `(fun) | b`.
194
+ // `fun()|nil` (a `|` right after the `)`) falls through to the union split.
195
+ if (/^fun\s*\(/.test(token)) {
196
+ const close = matchBracket(token, token.indexOf("("));
197
+ const afterClose = close === -1 ? "" : token.slice(close + 1).trim();
198
+ if (close !== -1 && afterClose.startsWith(":")) {
199
+ return mapFunction(token, ctx, unknowns);
200
+ }
201
+ }
202
+
203
+ // Top-level union.
204
+ const unionParts = splitTopLevel(token, "|");
205
+ if (unionParts.length > 1) {
206
+ return unionParts.map((p) => wrapForUnion(mapToken(p.trim(), ctx, unknowns))).join(" | ");
207
+ }
208
+
209
+ // Trailing array.
210
+ if (token.endsWith("[]")) {
211
+ const element = mapToken(token.slice(0, -2), ctx, unknowns);
212
+ return needsArrayParens(element) ? `(${element})[]` : `${element}[]`;
213
+ }
214
+
215
+ // Function.
216
+ if (/^fun\s*\(/.test(token)) return mapFunction(token, ctx, unknowns);
217
+
218
+ // Table.
219
+ if (token === "table") return "LuaTable";
220
+ if (token.startsWith("table<") && token.endsWith(">")) {
221
+ const args = splitTopLevel(token.slice(6, -1), ",").map((a) =>
222
+ mapToken(a.trim(), ctx, unknowns),
223
+ );
224
+ return `LuaTable<${args.join(", ")}>`;
225
+ }
226
+
227
+ // Inline object.
228
+ if (token.startsWith("{") && token.endsWith("}")) return mapObject(token, ctx, unknowns);
229
+
230
+ // String literal — passthrough.
231
+ if (token.startsWith('"') && token.endsWith('"')) return token;
232
+
233
+ // Scalars.
234
+ const scalar = SCALARS[token];
235
+ if (scalar !== undefined) return scalar;
236
+
237
+ // Reference-token precedence: per-target rename, core rename, loud-fail on an
238
+ // unmapped `vmath.*`, known model reference verbatim, else recorded `unknown`.
239
+ const override = ctx.typeRenames[token];
240
+ if (override !== undefined) return override;
241
+ const core = CORE_TYPE_RENAMES[token];
242
+ if (core !== undefined) return core;
243
+ if (token.startsWith("vmath.")) {
244
+ throw new Error(
245
+ `luals type mapper: unmapped Defold core token "${token}" - extend CORE_TYPE_RENAMES or the target's typeRenames.`,
246
+ );
247
+ }
248
+ if (ctx.knownNames.has(token)) return token;
249
+ unknowns.push(token);
250
+ return "unknown";
251
+ }
252
+
253
+ /** Map one raw LuaLS type token to a TypeScript type string. */
254
+ export function mapLualsType(token: string, ctx: MapContext): MapResult {
255
+ const unknowns: string[] = [];
256
+ const ts = mapToken(token, ctx, unknowns);
257
+ return { ts, unknowns };
258
+ }