@defold-typescript/library-types 0.22.0 → 0.23.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-doc/{bridge.bridge.json → bridge.json} +749 -844
- package/api-doc/decore.json +22 -22
- package/api-doc/druid.json +195 -1790
- package/api-doc/event.json +1074 -0
- package/api-doc/immutable.json +65 -0
- package/api-doc/lang.json +528 -0
- package/api-doc/{event.event.json → log.json} +96 -101
- package/api-doc/narrator.json +647 -0
- package/api-doc/proto.json +995 -0
- package/api-doc/saver.saver.json +713 -263
- package/api-doc/saver.storage.json +282 -52
- package/api-doc/squid.json +846 -0
- package/api-doc/tweener.json +277 -0
- package/generated/bridge.d.ts +468 -0
- package/generated/decore.d.ts +36 -36
- package/generated/druid.d.ts +137 -443
- package/generated/event.d.ts +318 -0
- package/generated/immutable.d.ts +13 -0
- package/generated/lang.d.ts +101 -0
- package/generated/log.d.ts +36 -0
- package/generated/narrator.d.ts +121 -0
- package/generated/proto.d.ts +146 -0
- package/generated/saver.saver.d.ts +287 -42
- package/generated/saver.storage.d.ts +77 -14
- package/generated/squid.d.ts +127 -0
- package/generated/tweener.d.ts +42 -0
- package/library-classification.json +0 -71
- package/library-targets.json +0 -66
- package/luals-targets.json +114 -0
- package/package.json +4 -33
- package/script-api-targets.json +15 -0
- package/scripts/__snapshots__/parse-luals.test.ts.snap +340 -75
- package/scripts/apply-luals-overrides.ts +63 -0
- package/scripts/emit-library-dts.ts +84 -3
- package/scripts/lower-api-doc.ts +48 -16
- package/scripts/luals-fidelity.ts +7 -0
- package/scripts/map-luals-types.ts +34 -1
- package/scripts/parse-luals.ts +423 -18
- package/scripts/sync-luals-types.ts +15 -1
- package/scripts/sync-script-api-types.ts +368 -0
- package/api-doc/immutable.immutable.json +0 -63
- package/api-doc/lang.lang.json +0 -411
- package/api-doc/log.log.json +0 -50
- package/api-doc/narrator.narrator.json +0 -150
- package/api-doc/proto.proto.json +0 -355
- package/api-doc/squid.squid.json +0 -660
- package/api-doc/tweener.tweener.json +0 -419
- package/generated/bridge.bridge.d.ts +0 -533
- package/generated/event.event.d.ts +0 -54
- package/generated/immutable.immutable.d.ts +0 -13
- package/generated/lang.lang.d.ts +0 -33
- package/generated/log.log.d.ts +0 -40
- package/generated/narrator.narrator.d.ts +0 -66
- package/generated/proto.proto.d.ts +0 -36
- package/generated/squid.squid.d.ts +0 -106
- package/generated/tweener.tweener.d.ts +0 -151
package/scripts/parse-luals.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface LibraryModel {
|
|
|
16
16
|
interfaces: LibraryInterface[];
|
|
17
17
|
aliases: LibraryAlias[];
|
|
18
18
|
moduleFunctions: LibraryMethod[];
|
|
19
|
+
// The name of the `---@class` a `return <name>` at column 0 hands back as the module
|
|
20
|
+
// table. Its public fields are the module's own constants (`export const`s), not a
|
|
21
|
+
// standalone interface. Set only when a returned local resolves to an opened class,
|
|
22
|
+
// so a module with a plain returned table carries no key.
|
|
23
|
+
moduleObject?: string;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface LibraryInterface {
|
|
@@ -25,6 +30,16 @@ export interface LibraryInterface {
|
|
|
25
30
|
fields: LibraryField[];
|
|
26
31
|
methods: LibraryMethod[];
|
|
27
32
|
brief: string;
|
|
33
|
+
// A class-level `---@overload fun(...)`, kept as its raw `fun(...)` token plus the
|
|
34
|
+
// trailing description. Present only on interfaces that declare one (like `extends`),
|
|
35
|
+
// so an interface without overloads carries no key. The emitter renders each as an
|
|
36
|
+
// interface call signature; the mapper maps the token to a `(params): ret` form.
|
|
37
|
+
overloads?: LibraryOverload[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LibraryOverload {
|
|
41
|
+
type: string;
|
|
42
|
+
doc: string;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
export interface LibraryMethod {
|
|
@@ -33,6 +48,11 @@ export interface LibraryMethod {
|
|
|
33
48
|
generics: LibraryGeneric[];
|
|
34
49
|
params: LibraryParam[];
|
|
35
50
|
returns: LibraryParam[];
|
|
51
|
+
// A standalone `---@local`/`---@private`/`---@protected`/`---@package` before the
|
|
52
|
+
// function declaration. Set only when marked (like a field's `visibility`), so an
|
|
53
|
+
// unmarked method carries no key. `local` has no `@field` analogue (LuaLS field
|
|
54
|
+
// scope has no `local`), so a method's visibility widens the field set with it.
|
|
55
|
+
visibility?: LibraryMethodVisibility;
|
|
36
56
|
}
|
|
37
57
|
|
|
38
58
|
export interface LibraryParam {
|
|
@@ -41,10 +61,17 @@ export interface LibraryParam {
|
|
|
41
61
|
doc: string;
|
|
42
62
|
isOptional: boolean;
|
|
43
63
|
isVararg: boolean;
|
|
64
|
+
// True when the raw type token carries a top-level `nil` union member (`T|nil`),
|
|
65
|
+
// distinct from the literal trailing `?` that drives `isOptional`. Set only when
|
|
66
|
+
// true (like a field's `visibility`), so a non-nil-bearing param carries no key.
|
|
67
|
+
// The emitter's trailing-run rule treats `isOptional || isNilable` as omittable.
|
|
68
|
+
isNilable?: boolean;
|
|
44
69
|
}
|
|
45
70
|
|
|
46
71
|
export type LibraryFieldVisibility = "public" | "protected" | "private" | "package";
|
|
47
72
|
|
|
73
|
+
export type LibraryMethodVisibility = LibraryFieldVisibility | "local";
|
|
74
|
+
|
|
48
75
|
export interface LibraryField {
|
|
49
76
|
name: string;
|
|
50
77
|
types: string[];
|
|
@@ -69,9 +96,17 @@ interface Pending {
|
|
|
69
96
|
params: LibraryParam[];
|
|
70
97
|
returns: LibraryParam[];
|
|
71
98
|
generics: LibraryGeneric[];
|
|
99
|
+
overloads: LibraryOverload[];
|
|
100
|
+
visibility?: LibraryMethodVisibility;
|
|
72
101
|
}
|
|
73
102
|
|
|
74
|
-
const emptyPending = (): Pending => ({
|
|
103
|
+
const emptyPending = (): Pending => ({
|
|
104
|
+
doc: [],
|
|
105
|
+
params: [],
|
|
106
|
+
returns: [],
|
|
107
|
+
generics: [],
|
|
108
|
+
overloads: [],
|
|
109
|
+
});
|
|
75
110
|
|
|
76
111
|
/**
|
|
77
112
|
* Read a single raw type token from the head of `rest`, honoring bracket depth so
|
|
@@ -101,6 +136,170 @@ function readTypeToken(rest: string): { type: string; rest: string } {
|
|
|
101
136
|
return { type: rest.slice(0, i), rest: rest.slice(i).trim() };
|
|
102
137
|
}
|
|
103
138
|
|
|
139
|
+
/** Index of the bracket matching the opener at `open`, or -1 if unbalanced. */
|
|
140
|
+
function matchCloser(s: string, open: number): number {
|
|
141
|
+
const pairs: Record<string, string> = { "<": ">", "(": ")", "[": "]", "{": "}" };
|
|
142
|
+
const want = pairs[s[open] as string];
|
|
143
|
+
let depth = 0;
|
|
144
|
+
let inQuote = false;
|
|
145
|
+
for (let i = open; i < s.length; i++) {
|
|
146
|
+
const c = s[i];
|
|
147
|
+
if (inQuote) {
|
|
148
|
+
if (c === '"') inQuote = false;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (c === '"') inQuote = true;
|
|
152
|
+
else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
153
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") {
|
|
154
|
+
depth--;
|
|
155
|
+
if (depth === 0) return c === want ? i : -1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return -1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Split `s` on every top-level occurrence of the single-character `sep`, honoring
|
|
163
|
+
* bracket depth and double-quoted string literals so a separator nested inside
|
|
164
|
+
* `<...>`, `(...)`, `[...]`, `{...}`, or a `"..."` literal does not split. A parser-
|
|
165
|
+
* local copy of the mapper's identical helper — the parser is upstream of the mapper
|
|
166
|
+
* and must not import it (`hasTopLevelNil`'s comment).
|
|
167
|
+
*/
|
|
168
|
+
function splitTopLevel(s: string, sep: string): string[] {
|
|
169
|
+
const parts: string[] = [];
|
|
170
|
+
let depth = 0;
|
|
171
|
+
let inQuote = false;
|
|
172
|
+
let start = 0;
|
|
173
|
+
for (let i = 0; i < s.length; i++) {
|
|
174
|
+
const c = s[i];
|
|
175
|
+
if (inQuote) {
|
|
176
|
+
if (c === '"') inQuote = false;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (c === '"') inQuote = true;
|
|
180
|
+
else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
181
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
182
|
+
else if (depth === 0 && c === sep) {
|
|
183
|
+
parts.push(s.slice(start, i));
|
|
184
|
+
start = i + 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
parts.push(s.slice(start));
|
|
188
|
+
return parts;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Decompose a `fun(...)` token into a method's `params`/`returns`, dropping a leading
|
|
193
|
+
* `self`. Used for a function-local `---@class` member typed by `---@type fun(...)`:
|
|
194
|
+
* modeling it as a method (each param keeping its raw type token) rather than a field
|
|
195
|
+
* makes the emitter render a full-typed method instead of the lossy permissive hook
|
|
196
|
+
* `matchSelfHookField` would produce. An unbalanced token yields empty lists.
|
|
197
|
+
*/
|
|
198
|
+
function funToMethodParts(token: string): { params: LibraryParam[]; returns: LibraryParam[] } {
|
|
199
|
+
const open = token.indexOf("(");
|
|
200
|
+
const close = open === -1 ? -1 : matchCloser(token, open);
|
|
201
|
+
if (open === -1 || close === -1) return { params: [], returns: [] };
|
|
202
|
+
const paramsStr = token.slice(open + 1, close).trim();
|
|
203
|
+
const afterClose = token.slice(close + 1).trim();
|
|
204
|
+
|
|
205
|
+
const params: LibraryParam[] = [];
|
|
206
|
+
const rawParams = paramsStr === "" ? [] : splitTopLevel(paramsStr, ",");
|
|
207
|
+
for (const rawPart of rawParams) {
|
|
208
|
+
const part = rawPart.trim();
|
|
209
|
+
if (part === "") continue;
|
|
210
|
+
if (part.startsWith("...")) {
|
|
211
|
+
const after = part.slice(3).trim();
|
|
212
|
+
const type = after.startsWith(":") ? after.slice(1).trim() : "";
|
|
213
|
+
const vararg: LibraryParam = {
|
|
214
|
+
name: "...",
|
|
215
|
+
types: type ? [type] : [],
|
|
216
|
+
doc: "",
|
|
217
|
+
isOptional: false,
|
|
218
|
+
isVararg: true,
|
|
219
|
+
};
|
|
220
|
+
if (type && hasTopLevelNil(type)) vararg.isNilable = true;
|
|
221
|
+
params.push(vararg);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const colon = splitTopLevel(part, ":");
|
|
225
|
+
const rawName = (colon[0] ?? "").trim();
|
|
226
|
+
const typeExpr = colon.length >= 2 ? colon.slice(1).join(":").trim() : "";
|
|
227
|
+
const isOptional = rawName.endsWith("?");
|
|
228
|
+
const name = isOptional ? rawName.slice(0, -1) : rawName;
|
|
229
|
+
const param: LibraryParam = {
|
|
230
|
+
name,
|
|
231
|
+
types: typeExpr ? [typeExpr] : [],
|
|
232
|
+
doc: "",
|
|
233
|
+
isOptional,
|
|
234
|
+
isVararg: false,
|
|
235
|
+
};
|
|
236
|
+
if (typeExpr && hasTopLevelNil(typeExpr)) param.isNilable = true;
|
|
237
|
+
params.push(param);
|
|
238
|
+
}
|
|
239
|
+
if (params[0]?.name === "self") params.shift();
|
|
240
|
+
|
|
241
|
+
const returns: LibraryParam[] = [];
|
|
242
|
+
if (afterClose.startsWith(":")) {
|
|
243
|
+
const retStr = afterClose.slice(1).trim();
|
|
244
|
+
for (const raw of retStr === "" ? [] : splitTopLevel(retStr, ",")) {
|
|
245
|
+
const type = raw.trim();
|
|
246
|
+
if (type)
|
|
247
|
+
returns.push({ name: "", types: [type], doc: "", isOptional: false, isVararg: false });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { params, returns };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* True when the raw type token has a top-level `nil` union member (`T|nil`,
|
|
255
|
+
* `fun()|nil`) — the signal that a parameter is nil-bearing and can be emitted
|
|
256
|
+
* TS-optional. Bracket- and quote-depth aware so a `nil` nested in
|
|
257
|
+
* `table<...>`/`{...}`/a `"..."` literal does not count, and a `fun(...): ret|nil`
|
|
258
|
+
* return-union (whose `|nil` sits at depth 0 after the `)`) is recognized as the
|
|
259
|
+
* function's own return, not an outer nullable — only a `|nil` applied to the whole
|
|
260
|
+
* token flags the param. Self-contained: the parser is upstream of the mapper and
|
|
261
|
+
* must not import it.
|
|
262
|
+
*/
|
|
263
|
+
function hasTopLevelNil(rawToken: string): boolean {
|
|
264
|
+
let token = rawToken.trim();
|
|
265
|
+
while (token.startsWith("(") && matchCloser(token, 0) === token.length - 1) {
|
|
266
|
+
token = token.slice(1, -1).trim();
|
|
267
|
+
}
|
|
268
|
+
if (/^fun\s*\(/.test(token)) {
|
|
269
|
+
const close = matchCloser(token, token.indexOf("("));
|
|
270
|
+
if (
|
|
271
|
+
close !== -1 &&
|
|
272
|
+
token
|
|
273
|
+
.slice(close + 1)
|
|
274
|
+
.trim()
|
|
275
|
+
.startsWith(":")
|
|
276
|
+
)
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
// A type-suffix `T?` (`string?`, `fun()?`, `(a|b)?`) is nil-bearing. Placed after the
|
|
280
|
+
// fun-return guard so a nullable return (`fun(): a|nil`, `fun(): string?`) never flags.
|
|
281
|
+
if (token.endsWith("?")) return true;
|
|
282
|
+
const isNilSeg = (from: number, to: number): boolean => token.slice(from, to).trim() === "nil";
|
|
283
|
+
let depth = 0;
|
|
284
|
+
let inQuote = false;
|
|
285
|
+
let segStart = 0;
|
|
286
|
+
for (let i = 0; i < token.length; i++) {
|
|
287
|
+
const c = token[i];
|
|
288
|
+
if (inQuote) {
|
|
289
|
+
if (c === '"') inQuote = false;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (c === '"') inQuote = true;
|
|
293
|
+
else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
294
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
295
|
+
else if (depth === 0 && c === "|") {
|
|
296
|
+
if (isNilSeg(segStart, i)) return true;
|
|
297
|
+
segStart = i + 1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return isNilSeg(segStart, token.length);
|
|
301
|
+
}
|
|
302
|
+
|
|
104
303
|
/** A bare lowercase identifier — the shape druid uses for an optional `@return` name. */
|
|
105
304
|
const RETURN_NAME = /^[a-z_][A-Za-z0-9_]*$/;
|
|
106
305
|
|
|
@@ -112,7 +311,9 @@ function parseParam(rest: string): LibraryParam {
|
|
|
112
311
|
const isOptional = !isVararg && rawName.endsWith("?");
|
|
113
312
|
const name = isOptional ? rawName.slice(0, -1) : rawName;
|
|
114
313
|
const { type, rest: doc } = readTypeToken(afterName);
|
|
115
|
-
|
|
314
|
+
const param: LibraryParam = { name, types: type ? [type] : [], doc, isOptional, isVararg };
|
|
315
|
+
if (type && hasTopLevelNil(type)) param.isNilable = true;
|
|
316
|
+
return param;
|
|
116
317
|
}
|
|
117
318
|
|
|
118
319
|
function parseReturn(rest: string): LibraryParam {
|
|
@@ -152,9 +353,12 @@ function parseField(rest: string): LibraryField {
|
|
|
152
353
|
const spaceAt = body.search(/\s/);
|
|
153
354
|
const rawName = spaceAt === -1 ? body : body.slice(0, spaceAt);
|
|
154
355
|
const afterName = spaceAt === -1 ? "" : body.slice(spaceAt).trim();
|
|
155
|
-
const
|
|
156
|
-
const name =
|
|
356
|
+
const nameSuffix = rawName.endsWith("?");
|
|
357
|
+
const name = nameSuffix ? rawName.slice(0, -1) : rawName;
|
|
157
358
|
const { type, rest: doc } = readTypeToken(afterName);
|
|
359
|
+
// Interface properties carry no optional-before-required rule, so a `|nil`/`?` type
|
|
360
|
+
// folds straight into the field's optionality.
|
|
361
|
+
const isOptional = nameSuffix || (type !== "" && hasTopLevelNil(type));
|
|
158
362
|
return {
|
|
159
363
|
name,
|
|
160
364
|
types: type ? [type] : [],
|
|
@@ -166,7 +370,15 @@ function parseField(rest: string): LibraryField {
|
|
|
166
370
|
|
|
167
371
|
function parseVararg(rest: string): LibraryParam {
|
|
168
372
|
const { type, rest: doc } = readTypeToken(rest);
|
|
169
|
-
|
|
373
|
+
const param: LibraryParam = {
|
|
374
|
+
name: "...",
|
|
375
|
+
types: type ? [type] : [],
|
|
376
|
+
doc,
|
|
377
|
+
isOptional: false,
|
|
378
|
+
isVararg: true,
|
|
379
|
+
};
|
|
380
|
+
if (type && hasTopLevelNil(type)) param.isNilable = true;
|
|
381
|
+
return param;
|
|
170
382
|
}
|
|
171
383
|
|
|
172
384
|
function parseGenerics(rest: string): LibraryGeneric[] {
|
|
@@ -181,12 +393,50 @@ function parseGenerics(rest: string): LibraryGeneric[] {
|
|
|
181
393
|
});
|
|
182
394
|
}
|
|
183
395
|
|
|
184
|
-
/**
|
|
396
|
+
/**
|
|
397
|
+
* Read one class-head token from `s` starting at `start`, tracking `<[({` depth so a
|
|
398
|
+
* generic like `Bar<A, B>` stays whole. Ends at the first top-level space/tab, `:`, or
|
|
399
|
+
* `,` (none consumed). Returns the trimmed token and its end index. Distinct from
|
|
400
|
+
* `readTypeToken`, whose `fun(...)` `:`/`,` continuation rule wrongly glues a
|
|
401
|
+
* `Name: Parent` head together.
|
|
402
|
+
*/
|
|
403
|
+
function readClassToken(s: string, start: number): { token: string; end: number } {
|
|
404
|
+
let depth = 0;
|
|
405
|
+
let i = start;
|
|
406
|
+
for (; i < s.length; i++) {
|
|
407
|
+
const c = s[i];
|
|
408
|
+
if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
409
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
410
|
+
else if (depth === 0 && (c === " " || c === "\t" || c === ":" || c === ",")) break;
|
|
411
|
+
}
|
|
412
|
+
return { token: s.slice(start, i).trim(), end: i };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Parse a `@class Name[: parent[, parent...]] [description]` head. The identifier and
|
|
417
|
+
* optional parent list are read as bracket-aware tokens; any trailing human
|
|
418
|
+
* description is dropped. Multiple parents are joined with `", "` to match the single
|
|
419
|
+
* `extends` string the emitter renders.
|
|
420
|
+
*/
|
|
185
421
|
function parseClassHead(rest: string): { name: string; extends?: string } {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
422
|
+
const s = rest.trim();
|
|
423
|
+
const { token: name, end: afterName } = readClassToken(s, 0);
|
|
424
|
+
let i = afterName;
|
|
425
|
+
while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
|
|
426
|
+
if (s[i] !== ":") return { name };
|
|
427
|
+
i++;
|
|
428
|
+
const parents: string[] = [];
|
|
429
|
+
while (true) {
|
|
430
|
+
while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
|
|
431
|
+
const { token, end } = readClassToken(s, i);
|
|
432
|
+
if (token !== "") parents.push(token);
|
|
433
|
+
i = end;
|
|
434
|
+
while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
|
|
435
|
+
if (s[i] !== ",") break;
|
|
436
|
+
i++;
|
|
437
|
+
}
|
|
438
|
+
const extendsStr = parents.join(", ");
|
|
439
|
+
return { name, ...(extendsStr ? { extends: extendsStr } : {}) };
|
|
190
440
|
}
|
|
191
441
|
|
|
192
442
|
interface FunctionDecl {
|
|
@@ -260,6 +510,19 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
260
510
|
let pending = emptyPending();
|
|
261
511
|
let openClass: LibraryInterface | null = null;
|
|
262
512
|
let lastOpenedClass: string | null = null;
|
|
513
|
+
let moduleObject: string | undefined;
|
|
514
|
+
let lastModuleFunction: LibraryMethod | null = null;
|
|
515
|
+
// The one open function-local `---@class` block (squid's `SquidInstance` inside
|
|
516
|
+
// `Squid.new`). `owner` is the function whose body it sits in, so a returned local
|
|
517
|
+
// bound to it infers the function's return; `pendingType` is a `---@type fun(...)`
|
|
518
|
+
// armed for the next member key.
|
|
519
|
+
let localClass: {
|
|
520
|
+
iface: LibraryInterface;
|
|
521
|
+
localVar: string | null;
|
|
522
|
+
owner: LibraryMethod | null;
|
|
523
|
+
pendingType: string | null;
|
|
524
|
+
returnedSelf: boolean;
|
|
525
|
+
} | null = null;
|
|
263
526
|
|
|
264
527
|
const ensureInterface = (name: string): LibraryInterface => {
|
|
265
528
|
const existing = byName.get(name);
|
|
@@ -282,11 +545,93 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
282
545
|
generics: pending.generics,
|
|
283
546
|
params: pending.params,
|
|
284
547
|
returns: pending.returns,
|
|
548
|
+
...(pending.visibility ? { visibility: pending.visibility } : {}),
|
|
285
549
|
});
|
|
286
550
|
|
|
551
|
+
// Interpret one indented line while a function-local `---@class` block is open. Only
|
|
552
|
+
// the `---@type fun(...)` + next `<key> = ...` member pattern, the backing `local`,
|
|
553
|
+
// and the `return <local>` are recognized; every other indented line stays opaque.
|
|
554
|
+
const handleLocalClassLine = (line: string): void => {
|
|
555
|
+
const lc = localClass;
|
|
556
|
+
if (!lc) return;
|
|
557
|
+
const typeMatch = /^---@type\s+(.+)$/.exec(line);
|
|
558
|
+
if (typeMatch) {
|
|
559
|
+
const { type } = readTypeToken(typeMatch[1] ?? "");
|
|
560
|
+
// A non-`fun` `---@type` also disarms a previously-armed one.
|
|
561
|
+
lc.pendingType = /^fun\s*\(/.test(type) ? type : null;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (lc.pendingType) {
|
|
565
|
+
const keyMatch = /^([A-Za-z_]\w*)\s*=/.exec(line);
|
|
566
|
+
if (keyMatch) {
|
|
567
|
+
const { params, returns } = funToMethodParts(lc.pendingType);
|
|
568
|
+
lc.iface.methods.push({
|
|
569
|
+
name: keyMatch[1] as string,
|
|
570
|
+
brief: "",
|
|
571
|
+
generics: [],
|
|
572
|
+
params,
|
|
573
|
+
returns,
|
|
574
|
+
});
|
|
575
|
+
lc.pendingType = null;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Not the immediately-following member key: the armed type has no member.
|
|
579
|
+
lc.pendingType = null;
|
|
580
|
+
}
|
|
581
|
+
const returnMatch = /^return\s+([A-Za-z_]\w*)\s*$/.exec(line);
|
|
582
|
+
if (returnMatch) {
|
|
583
|
+
if (returnMatch[1] === lc.localVar) lc.returnedSelf = true;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const local = LOCAL_ASSIGN.exec(line);
|
|
587
|
+
if (local) {
|
|
588
|
+
// A `---@class` annotates the immediately-following declaration, so only the
|
|
589
|
+
// first local after the class opens is the instance; a later local must not rebind.
|
|
590
|
+
if (lc.localVar === null) lc.localVar = local[1] ?? null;
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
287
595
|
for (const raw of source.split("\n")) {
|
|
288
|
-
|
|
289
|
-
if (
|
|
596
|
+
const indented = /^\s/.test(raw);
|
|
597
|
+
if (indented || raw.length === 0) {
|
|
598
|
+
if (localClass) {
|
|
599
|
+
handleLocalClassLine(raw.trim());
|
|
600
|
+
} else if (indented) {
|
|
601
|
+
// Only an indented `---@class` opens function-local capture; every other
|
|
602
|
+
// indented line (druid-style `---@cast`/`---@type` narrowing) stays opaque.
|
|
603
|
+
const classMatch = /^---@class\s+(.+)$/.exec(raw.trim());
|
|
604
|
+
if (classMatch) {
|
|
605
|
+
const head = parseClassHead(classMatch[1] ?? "");
|
|
606
|
+
const iface = ensureInterface(head.name);
|
|
607
|
+
if (head.extends) iface.extends = head.extends;
|
|
608
|
+
localClass = {
|
|
609
|
+
iface,
|
|
610
|
+
localVar: null,
|
|
611
|
+
owner: lastModuleFunction,
|
|
612
|
+
pendingType: null,
|
|
613
|
+
returnedSelf: false,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// A column-0 line ends any open function-local class (dedent). Apply the inferred
|
|
621
|
+
// return to the owner only when it declared no explicit `---@return`.
|
|
622
|
+
if (localClass) {
|
|
623
|
+
const lc = localClass;
|
|
624
|
+
if (lc.returnedSelf && lc.owner && lc.owner.returns.length === 0) {
|
|
625
|
+
lc.owner.returns.push({
|
|
626
|
+
name: "",
|
|
627
|
+
types: [lc.iface.name],
|
|
628
|
+
doc: "",
|
|
629
|
+
isOptional: false,
|
|
630
|
+
isVararg: false,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
localClass = null;
|
|
634
|
+
}
|
|
290
635
|
|
|
291
636
|
if (raw.startsWith("---@")) {
|
|
292
637
|
const tagMatch = /^---@([a-zA-Z]+)\s*(.*)$/.exec(raw);
|
|
@@ -300,6 +645,7 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
300
645
|
if (head.extends) iface.extends = head.extends;
|
|
301
646
|
if (pending.doc.length > 0 && iface.brief === "") iface.brief = pending.doc.join("\n");
|
|
302
647
|
if (pending.generics.length > 0) iface.generics = pending.generics;
|
|
648
|
+
if (pending.overloads.length > 0) iface.overloads = pending.overloads;
|
|
303
649
|
openClass = iface;
|
|
304
650
|
lastOpenedClass = head.name;
|
|
305
651
|
pending = emptyPending();
|
|
@@ -325,6 +671,15 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
325
671
|
pending.generics.push(...parseGenerics(rest));
|
|
326
672
|
break;
|
|
327
673
|
}
|
|
674
|
+
case "overload": {
|
|
675
|
+
// A class-level `---@overload fun(...)`: keep the raw `fun(...)` token via
|
|
676
|
+
// readTypeToken (its spaced `): ret` return stays whole) plus the trailing
|
|
677
|
+
// doc, and transfer to the interface on the following `@class` — like brief
|
|
678
|
+
// and generics. A non-`fun` overload is outside the modeled subset and dropped.
|
|
679
|
+
const { type, rest: doc } = readTypeToken(rest);
|
|
680
|
+
if (/^fun\s*\(/.test(type)) pending.overloads.push({ type, doc });
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
328
683
|
case "alias": {
|
|
329
684
|
const spaceAt = rest.search(/\s/);
|
|
330
685
|
const name = spaceAt === -1 ? rest : rest.slice(0, spaceAt);
|
|
@@ -333,9 +688,19 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
333
688
|
pending = emptyPending();
|
|
334
689
|
break;
|
|
335
690
|
}
|
|
691
|
+
case "private":
|
|
692
|
+
case "protected":
|
|
693
|
+
case "package":
|
|
694
|
+
case "local": {
|
|
695
|
+
// A standalone visibility tag on the pending block marks the next function
|
|
696
|
+
// declaration's method/module-function `visibility` (a `@class` or `@alias`
|
|
697
|
+
// that consumes the block first resets it, so it never leaks onto a type).
|
|
698
|
+
pending.visibility = tag as LibraryMethodVisibility;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
336
701
|
default:
|
|
337
|
-
// @
|
|
338
|
-
//
|
|
702
|
+
// @cast, @type, @diagnostic, ... — outside the Druid subset; recognized as a
|
|
703
|
+
// tag and skipped, never treated as doc.
|
|
339
704
|
break;
|
|
340
705
|
}
|
|
341
706
|
continue;
|
|
@@ -348,12 +713,16 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
348
713
|
|
|
349
714
|
const decl = parseFunctionDecl(raw);
|
|
350
715
|
if (decl) {
|
|
716
|
+
const method = methodFromPending(decl.name);
|
|
351
717
|
if (decl.kind === "method") {
|
|
352
718
|
const target = decl.receiver ? (receiverBinding.get(decl.receiver) ?? decl.receiver) : "";
|
|
353
|
-
ensureInterface(target).methods.push(
|
|
719
|
+
ensureInterface(target).methods.push(method);
|
|
354
720
|
} else if (decl.qualified) {
|
|
355
|
-
moduleFunctions.push(
|
|
721
|
+
moduleFunctions.push(method);
|
|
356
722
|
}
|
|
723
|
+
// Any function may host a function-local `---@class`; remember the enclosing
|
|
724
|
+
// function so a returned local can infer its return, even a bare helper.
|
|
725
|
+
lastModuleFunction = method;
|
|
357
726
|
pending = emptyPending();
|
|
358
727
|
openClass = null;
|
|
359
728
|
continue;
|
|
@@ -366,10 +735,34 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
366
735
|
lastOpenedClass = null;
|
|
367
736
|
openClass = null;
|
|
368
737
|
pending = emptyPending();
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// A column-0 `return <name>` handing back a local bound to an opened `---@class`
|
|
742
|
+
// marks that class as the module object (its fields become module-level consts).
|
|
743
|
+
// Restricted to squid's constants-table idiom to keep every other library's golden
|
|
744
|
+
// untouched: the returned local is named after its class (`local Squid = {}` /
|
|
745
|
+
// `return Squid`, not a generic `local M` alias), the class carries at least one
|
|
746
|
+
// public field, and it has no methods. A component/instance class (colon methods),
|
|
747
|
+
// an opaque handle (no fields), or an `M`-aliased module table is a type consumers
|
|
748
|
+
// reference or a plain namespace, so it stays a standalone interface.
|
|
749
|
+
const returnStmt = /^return\s+([A-Za-z_][\w.]*)\s*$/.exec(raw);
|
|
750
|
+
if (returnStmt) {
|
|
751
|
+
const name = returnStmt[1] as string;
|
|
752
|
+
const resolvedName = receiverBinding.get(name) ?? (byName.has(name) ? name : undefined);
|
|
753
|
+
const resolved = resolvedName ? byName.get(resolvedName) : undefined;
|
|
754
|
+
if (
|
|
755
|
+
resolved &&
|
|
756
|
+
resolvedName === name &&
|
|
757
|
+
resolved.methods.length === 0 &&
|
|
758
|
+
resolved.fields.some((f) => f.visibility === undefined || f.visibility === "public")
|
|
759
|
+
) {
|
|
760
|
+
moduleObject = resolved.name;
|
|
761
|
+
}
|
|
369
762
|
}
|
|
370
763
|
}
|
|
371
764
|
|
|
372
|
-
return { interfaces, aliases, moduleFunctions };
|
|
765
|
+
return { interfaces, aliases, moduleFunctions, ...(moduleObject ? { moduleObject } : {}) };
|
|
373
766
|
}
|
|
374
767
|
|
|
375
768
|
/**
|
|
@@ -383,8 +776,10 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
|
383
776
|
const byName = new Map<string, LibraryInterface>();
|
|
384
777
|
const aliases: LibraryAlias[] = [];
|
|
385
778
|
const moduleFunctions: LibraryMethod[] = [];
|
|
779
|
+
let moduleObject: string | undefined;
|
|
386
780
|
|
|
387
781
|
for (const model of models) {
|
|
782
|
+
if (!moduleObject && model.moduleObject) moduleObject = model.moduleObject;
|
|
388
783
|
for (const iface of model.interfaces) {
|
|
389
784
|
const existing = byName.get(iface.name);
|
|
390
785
|
if (!existing) {
|
|
@@ -395,6 +790,9 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
|
395
790
|
fields: [...iface.fields],
|
|
396
791
|
methods: [...iface.methods],
|
|
397
792
|
brief: iface.brief,
|
|
793
|
+
...(iface.overloads && iface.overloads.length > 0
|
|
794
|
+
? { overloads: [...iface.overloads] }
|
|
795
|
+
: {}),
|
|
398
796
|
};
|
|
399
797
|
byName.set(iface.name, copy);
|
|
400
798
|
interfaces.push(copy);
|
|
@@ -407,6 +805,13 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
|
407
805
|
if (existing.generics.length === 0 && iface.generics.length > 0) {
|
|
408
806
|
existing.generics = [...iface.generics];
|
|
409
807
|
}
|
|
808
|
+
if (
|
|
809
|
+
(!existing.overloads || existing.overloads.length === 0) &&
|
|
810
|
+
iface.overloads &&
|
|
811
|
+
iface.overloads.length > 0
|
|
812
|
+
) {
|
|
813
|
+
existing.overloads = [...iface.overloads];
|
|
814
|
+
}
|
|
410
815
|
}
|
|
411
816
|
aliases.push(...model.aliases);
|
|
412
817
|
moduleFunctions.push(...model.moduleFunctions);
|
|
@@ -419,7 +824,7 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
|
419
824
|
// overloaded module functions keep every signature.
|
|
420
825
|
for (const iface of interfaces) iface.fields = dedupeByName(iface.fields);
|
|
421
826
|
|
|
422
|
-
return { interfaces, aliases, moduleFunctions };
|
|
827
|
+
return { interfaces, aliases, moduleFunctions, ...(moduleObject ? { moduleObject } : {}) };
|
|
423
828
|
}
|
|
424
829
|
|
|
425
830
|
function dedupeByName<T extends { name: string }>(items: T[]): T[] {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { type AnnotationOverrides, applyAnnotationOverrides } from "./apply-luals-overrides";
|
|
3
4
|
import { emitLibraryDeclarations } from "./emit-library-dts";
|
|
4
5
|
import { lowerLibraryModel } from "./lower-api-doc";
|
|
5
6
|
import { buildFidelityReport, type FidelityReport } from "./luals-fidelity";
|
|
@@ -22,6 +23,10 @@ export interface LualsTarget {
|
|
|
22
23
|
// SPDX-style license id, surfaced by the docs-site provenance block. Optional
|
|
23
24
|
// in the config; the docs-site defaults an absent value to "".
|
|
24
25
|
license?: string;
|
|
26
|
+
// Post-merge corrections applied in `buildTargetModel` for the cases where an
|
|
27
|
+
// upstream annotation the fixtures freeze diverges from the runtime. Optional and
|
|
28
|
+
// loud-failing on an absent key — see `apply-luals-overrides.ts`.
|
|
29
|
+
annotationOverrides?: AnnotationOverrides;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export interface LualsTargets {
|
|
@@ -57,6 +62,9 @@ export function readLualsTargets(packageRoot: string): LualsTarget[] {
|
|
|
57
62
|
typeRenames: entry.typeRenames ?? {},
|
|
58
63
|
ignore: entry.ignore ?? [],
|
|
59
64
|
...(entry.license !== undefined ? { license: entry.license } : {}),
|
|
65
|
+
...(entry.annotationOverrides !== undefined
|
|
66
|
+
? { annotationOverrides: entry.annotationOverrides }
|
|
67
|
+
: {}),
|
|
60
68
|
};
|
|
61
69
|
});
|
|
62
70
|
}
|
|
@@ -188,7 +196,13 @@ export function buildTargetModel(
|
|
|
188
196
|
const parsed = new Map<string, LibraryModel>();
|
|
189
197
|
for (const rel of files) parsed.set(rel, parseLualsSource(readFileSync(join(root, rel), "utf8")));
|
|
190
198
|
const merged = mergeLibraryModels([...parsed.values()]);
|
|
191
|
-
|
|
199
|
+
const model: LibraryModel = {
|
|
200
|
+
...merged,
|
|
201
|
+
moduleFunctions: parsed.get(ownFile)?.moduleFunctions ?? [],
|
|
202
|
+
};
|
|
203
|
+
return target.annotationOverrides
|
|
204
|
+
? applyAnnotationOverrides(model, target.annotationOverrides)
|
|
205
|
+
: model;
|
|
192
206
|
}
|
|
193
207
|
|
|
194
208
|
export function buildTargetFidelity(packageRoot: string, target: LualsTarget): FidelityReport {
|