@defold-typescript/library-types 0.22.0 → 0.24.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/boom.json +3870 -0
- package/api-doc/{bridge.bridge.json → bridge.json} +754 -844
- package/api-doc/bzAnim.json +338 -0
- package/api-doc/decore.json +23 -23
- package/api-doc/defcon.json +150 -0
- package/api-doc/{defmath.defmath.json → defmath.json} +6 -46
- package/api-doc/defsave.json +518 -0
- package/api-doc/deftest.json +741 -0
- package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
- package/api-doc/druid.json +249 -1846
- package/api-doc/event.json +1074 -0
- package/api-doc/gooey.json +2050 -0
- package/api-doc/immutable.json +65 -0
- package/api-doc/in.accelerometer.json +75 -1
- package/api-doc/in.button.json +17 -10
- package/api-doc/in.cursor.json +26 -0
- package/api-doc/in.gesture.json +401 -6
- package/api-doc/in.keyboard.json +2 -0
- package/api-doc/in.mapper.json +12 -8
- package/api-doc/in.onscreen.json +61 -5
- package/api-doc/in.state.json +61 -13
- package/api-doc/in.textbox.json +23 -12
- package/api-doc/in.triggers.json +336 -0
- package/api-doc/lang.json +528 -0
- package/api-doc/{event.event.json → log.json} +96 -101
- package/api-doc/metrics.fps.json +61 -2
- package/api-doc/metrics.mem.json +61 -2
- package/api-doc/monarch.monarch.json +249 -7
- package/api-doc/monarch.transitions.easings.json +19 -26
- package/api-doc/monarch.transitions.gui.json +782 -79
- package/api-doc/nakama.engine.defold.json +31 -18
- package/api-doc/{nakama.nakama.json → nakama.json} +4752 -2554
- package/api-doc/nakama.util.log.json +32 -4
- package/api-doc/narrator.json +647 -0
- package/api-doc/openapi/nakama.nakama.json +7680 -0
- package/api-doc/orthographic.camera.json +96 -98
- package/api-doc/orthographic.json +602 -0
- package/api-doc/panthera.json +1183 -0
- package/api-doc/{persist.persist.json → persist.json} +26 -1
- package/api-doc/{platypus.platypus.json → platypus.json} +68 -14
- package/api-doc/proto.json +995 -0
- package/api-doc/{rendy.rendy.json → rendy.json} +46 -99
- package/api-doc/richtext.color.json +77 -0
- package/api-doc/richtext.richtext.json +375 -5
- package/api-doc/saver.saver.json +714 -263
- package/api-doc/saver.storage.json +283 -52
- package/api-doc/squid.json +846 -0
- package/api-doc/{starly.starly.json → starly.json} +1 -1
- package/api-doc/tweener.json +277 -0
- package/api-doc/{yagames.yagames.json → yagames.json} +784 -189
- package/api-doc/{zzfx.api.json → zzfx.json} +9 -9
- package/authored-targets.json +403 -0
- package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
- package/generated/bridge.d.ts +468 -0
- package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
- package/generated/decore.d.ts +37 -37
- package/generated/defcon.d.ts +43 -0
- package/generated/{defmath.defmath.d.ts → defmath.d.ts} +3 -2
- package/generated/defsave.d.ts +144 -0
- package/generated/{dicebag.dicebag.d.ts → dicebag.d.ts} +25 -0
- package/generated/druid.d.ts +198 -502
- package/generated/event.d.ts +318 -0
- package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
- package/generated/immutable.d.ts +13 -0
- package/generated/in.onscreen.d.ts +5 -0
- package/generated/in.textbox.d.ts +4 -0
- package/generated/lang.d.ts +101 -0
- package/generated/log.d.ts +36 -0
- package/generated/metrics.fps.d.ts +19 -1
- package/generated/metrics.mem.d.ts +19 -1
- package/generated/monarch.monarch.d.ts +44 -1
- package/generated/monarch.transitions.easings.d.ts +0 -1
- package/generated/monarch.transitions.gui.d.ts +96 -12
- package/generated/nakama.d.ts +1158 -0
- package/generated/nakama.engine.defold.d.ts +3 -1
- package/generated/nakama.util.log.d.ts +6 -0
- package/generated/narrator.d.ts +121 -0
- package/generated/openapi/nakama.nakama.d.ts +884 -0
- package/generated/orthographic.camera.d.ts +24 -38
- package/generated/orthographic.d.ts +155 -0
- package/generated/panthera.d.ts +170 -0
- package/generated/{persist.persist.d.ts → persist.d.ts} +3 -0
- package/generated/{platypus.platypus.d.ts → platypus.d.ts} +9 -0
- package/generated/proto.d.ts +146 -0
- package/generated/{rendy.rendy.d.ts → rendy.d.ts} +33 -80
- package/generated/richtext.color.d.ts +15 -0
- package/generated/saver.saver.d.ts +289 -42
- package/generated/saver.storage.d.ts +79 -14
- package/generated/squid.d.ts +127 -0
- package/generated/tweener.d.ts +42 -0
- package/generated/{yagames.yagames.d.ts → yagames.d.ts} +151 -29
- package/generated/{zzfx.api.d.ts → zzfx.d.ts} +7 -0
- package/library-classification.json +0 -227
- package/library-targets.json +1 -284
- package/luals-targets.json +168 -1
- package/openapi-targets.json +17 -0
- package/package.json +9 -88
- package/script-api-targets.json +15 -0
- package/scripts/__snapshots__/parse-luals.test.ts.snap +504 -85
- package/scripts/apply-luals-overrides.ts +95 -0
- package/scripts/authored-parity.ts +718 -0
- package/scripts/emit-library-dts.ts +177 -19
- package/scripts/extract-api-doc.ts +189 -38
- package/scripts/fidelity-floor.ts +178 -0
- package/scripts/import-upstream-docs.ts +125 -0
- package/scripts/lower-api-doc.ts +69 -19
- package/scripts/luals-fidelity.ts +22 -10
- package/scripts/map-luals-types.ts +86 -22
- package/scripts/parse-lua-surface.ts +187 -0
- package/scripts/parse-luals.ts +473 -22
- package/scripts/parse-markdown-api.ts +247 -0
- package/scripts/parse-openapi-api.ts +313 -0
- package/scripts/sync-authored-types.ts +249 -0
- package/scripts/sync-library-types.ts +99 -3
- package/scripts/sync-luals-types.ts +29 -2
- package/scripts/sync-markdown-types.ts +761 -0
- package/scripts/sync-openapi-types.ts +308 -0
- package/scripts/sync-script-api-types.ts +367 -0
- package/api-doc/boom.boom.json +0 -26
- package/api-doc/bzAnim.bzLibrary.json +0 -156
- package/api-doc/defcon.console.json +0 -109
- package/api-doc/defsave.defsave.json +0 -175
- package/api-doc/deftest.deftest.json +0 -71
- package/api-doc/gooey.gooey.json +0 -963
- 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/defcon.console.d.ts +0 -24
- package/generated/defsave.defsave.d.ts +0 -31
- 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/nakama.nakama.d.ts +0 -594
- 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/generated/{deftest.deftest.d.ts → deftest.d.ts} +0 -0
- /package/generated/{starly.starly.d.ts → starly.d.ts} +0 -0
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,17 @@ 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
|
+
deprecated?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface LibraryOverload {
|
|
42
|
+
type: string;
|
|
43
|
+
doc: string;
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
export interface LibraryMethod {
|
|
@@ -33,6 +49,15 @@ export interface LibraryMethod {
|
|
|
33
49
|
generics: LibraryGeneric[];
|
|
34
50
|
params: LibraryParam[];
|
|
35
51
|
returns: LibraryParam[];
|
|
52
|
+
// A standalone `---@local`/`---@private`/`---@protected`/`---@package` before the
|
|
53
|
+
// function declaration. Set only when marked (like a field's `visibility`), so an
|
|
54
|
+
// unmarked method carries no key. `local` has no `@field` analogue (LuaLS field
|
|
55
|
+
// scope has no `local`), so a method's visibility widens the field set with it.
|
|
56
|
+
visibility?: LibraryMethodVisibility;
|
|
57
|
+
// A `---@deprecated` on the pending block. Present exactly when the tag is, with
|
|
58
|
+
// `""` for the bare form — absence of the key is the sole encoding of "not
|
|
59
|
+
// deprecated", so a bare tag must never be collapsed away by a truthiness guard.
|
|
60
|
+
deprecated?: string;
|
|
36
61
|
}
|
|
37
62
|
|
|
38
63
|
export interface LibraryParam {
|
|
@@ -41,10 +66,17 @@ export interface LibraryParam {
|
|
|
41
66
|
doc: string;
|
|
42
67
|
isOptional: boolean;
|
|
43
68
|
isVararg: boolean;
|
|
69
|
+
// True when the raw type token carries a top-level `nil` union member (`T|nil`),
|
|
70
|
+
// distinct from the literal trailing `?` that drives `isOptional`. Set only when
|
|
71
|
+
// true (like a field's `visibility`), so a non-nil-bearing param carries no key.
|
|
72
|
+
// The emitter's trailing-run rule treats `isOptional || isNilable` as omittable.
|
|
73
|
+
isNilable?: boolean;
|
|
44
74
|
}
|
|
45
75
|
|
|
46
76
|
export type LibraryFieldVisibility = "public" | "protected" | "private" | "package";
|
|
47
77
|
|
|
78
|
+
export type LibraryMethodVisibility = LibraryFieldVisibility | "local";
|
|
79
|
+
|
|
48
80
|
export interface LibraryField {
|
|
49
81
|
name: string;
|
|
50
82
|
types: string[];
|
|
@@ -62,6 +94,7 @@ export interface LibraryAlias {
|
|
|
62
94
|
name: string;
|
|
63
95
|
types: string[];
|
|
64
96
|
doc: string;
|
|
97
|
+
deprecated?: string;
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
interface Pending {
|
|
@@ -69,9 +102,18 @@ interface Pending {
|
|
|
69
102
|
params: LibraryParam[];
|
|
70
103
|
returns: LibraryParam[];
|
|
71
104
|
generics: LibraryGeneric[];
|
|
105
|
+
overloads: LibraryOverload[];
|
|
106
|
+
visibility?: LibraryMethodVisibility;
|
|
107
|
+
deprecated?: string;
|
|
72
108
|
}
|
|
73
109
|
|
|
74
|
-
const emptyPending = (): Pending => ({
|
|
110
|
+
const emptyPending = (): Pending => ({
|
|
111
|
+
doc: [],
|
|
112
|
+
params: [],
|
|
113
|
+
returns: [],
|
|
114
|
+
generics: [],
|
|
115
|
+
overloads: [],
|
|
116
|
+
});
|
|
75
117
|
|
|
76
118
|
/**
|
|
77
119
|
* Read a single raw type token from the head of `rest`, honoring bracket depth so
|
|
@@ -101,6 +143,170 @@ function readTypeToken(rest: string): { type: string; rest: string } {
|
|
|
101
143
|
return { type: rest.slice(0, i), rest: rest.slice(i).trim() };
|
|
102
144
|
}
|
|
103
145
|
|
|
146
|
+
/** Index of the bracket matching the opener at `open`, or -1 if unbalanced. */
|
|
147
|
+
function matchCloser(s: string, open: number): number {
|
|
148
|
+
const pairs: Record<string, string> = { "<": ">", "(": ")", "[": "]", "{": "}" };
|
|
149
|
+
const want = pairs[s[open] as string];
|
|
150
|
+
let depth = 0;
|
|
151
|
+
let inQuote = false;
|
|
152
|
+
for (let i = open; i < s.length; i++) {
|
|
153
|
+
const c = s[i];
|
|
154
|
+
if (inQuote) {
|
|
155
|
+
if (c === '"') inQuote = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (c === '"') inQuote = true;
|
|
159
|
+
else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
160
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") {
|
|
161
|
+
depth--;
|
|
162
|
+
if (depth === 0) return c === want ? i : -1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return -1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Split `s` on every top-level occurrence of the single-character `sep`, honoring
|
|
170
|
+
* bracket depth and double-quoted string literals so a separator nested inside
|
|
171
|
+
* `<...>`, `(...)`, `[...]`, `{...}`, or a `"..."` literal does not split. A parser-
|
|
172
|
+
* local copy of the mapper's identical helper — the parser is upstream of the mapper
|
|
173
|
+
* and must not import it (`hasTopLevelNil`'s comment).
|
|
174
|
+
*/
|
|
175
|
+
function splitTopLevel(s: string, sep: string): string[] {
|
|
176
|
+
const parts: string[] = [];
|
|
177
|
+
let depth = 0;
|
|
178
|
+
let inQuote = false;
|
|
179
|
+
let start = 0;
|
|
180
|
+
for (let i = 0; i < s.length; i++) {
|
|
181
|
+
const c = s[i];
|
|
182
|
+
if (inQuote) {
|
|
183
|
+
if (c === '"') inQuote = false;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (c === '"') inQuote = true;
|
|
187
|
+
else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
188
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
189
|
+
else if (depth === 0 && c === sep) {
|
|
190
|
+
parts.push(s.slice(start, i));
|
|
191
|
+
start = i + 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
parts.push(s.slice(start));
|
|
195
|
+
return parts;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Decompose a `fun(...)` token into a method's `params`/`returns`, dropping a leading
|
|
200
|
+
* `self`. Used for a function-local `---@class` member typed by `---@type fun(...)`:
|
|
201
|
+
* modeling it as a method (each param keeping its raw type token) rather than a field
|
|
202
|
+
* makes the emitter render a full-typed method instead of the lossy permissive hook
|
|
203
|
+
* `matchSelfHookField` would produce. An unbalanced token yields empty lists.
|
|
204
|
+
*/
|
|
205
|
+
function funToMethodParts(token: string): { params: LibraryParam[]; returns: LibraryParam[] } {
|
|
206
|
+
const open = token.indexOf("(");
|
|
207
|
+
const close = open === -1 ? -1 : matchCloser(token, open);
|
|
208
|
+
if (open === -1 || close === -1) return { params: [], returns: [] };
|
|
209
|
+
const paramsStr = token.slice(open + 1, close).trim();
|
|
210
|
+
const afterClose = token.slice(close + 1).trim();
|
|
211
|
+
|
|
212
|
+
const params: LibraryParam[] = [];
|
|
213
|
+
const rawParams = paramsStr === "" ? [] : splitTopLevel(paramsStr, ",");
|
|
214
|
+
for (const rawPart of rawParams) {
|
|
215
|
+
const part = rawPart.trim();
|
|
216
|
+
if (part === "") continue;
|
|
217
|
+
if (part.startsWith("...")) {
|
|
218
|
+
const after = part.slice(3).trim();
|
|
219
|
+
const type = after.startsWith(":") ? after.slice(1).trim() : "";
|
|
220
|
+
const vararg: LibraryParam = {
|
|
221
|
+
name: "...",
|
|
222
|
+
types: type ? [type] : [],
|
|
223
|
+
doc: "",
|
|
224
|
+
isOptional: false,
|
|
225
|
+
isVararg: true,
|
|
226
|
+
};
|
|
227
|
+
if (type && hasTopLevelNil(type)) vararg.isNilable = true;
|
|
228
|
+
params.push(vararg);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const colon = splitTopLevel(part, ":");
|
|
232
|
+
const rawName = (colon[0] ?? "").trim();
|
|
233
|
+
const typeExpr = colon.length >= 2 ? colon.slice(1).join(":").trim() : "";
|
|
234
|
+
const isOptional = rawName.endsWith("?");
|
|
235
|
+
const name = isOptional ? rawName.slice(0, -1) : rawName;
|
|
236
|
+
const param: LibraryParam = {
|
|
237
|
+
name,
|
|
238
|
+
types: typeExpr ? [typeExpr] : [],
|
|
239
|
+
doc: "",
|
|
240
|
+
isOptional,
|
|
241
|
+
isVararg: false,
|
|
242
|
+
};
|
|
243
|
+
if (typeExpr && hasTopLevelNil(typeExpr)) param.isNilable = true;
|
|
244
|
+
params.push(param);
|
|
245
|
+
}
|
|
246
|
+
if (params[0]?.name === "self") params.shift();
|
|
247
|
+
|
|
248
|
+
const returns: LibraryParam[] = [];
|
|
249
|
+
if (afterClose.startsWith(":")) {
|
|
250
|
+
const retStr = afterClose.slice(1).trim();
|
|
251
|
+
for (const raw of retStr === "" ? [] : splitTopLevel(retStr, ",")) {
|
|
252
|
+
const type = raw.trim();
|
|
253
|
+
if (type)
|
|
254
|
+
returns.push({ name: "", types: [type], doc: "", isOptional: false, isVararg: false });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { params, returns };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* True when the raw type token has a top-level `nil` union member (`T|nil`,
|
|
262
|
+
* `fun()|nil`) — the signal that a parameter is nil-bearing and can be emitted
|
|
263
|
+
* TS-optional. Bracket- and quote-depth aware so a `nil` nested in
|
|
264
|
+
* `table<...>`/`{...}`/a `"..."` literal does not count, and a `fun(...): ret|nil`
|
|
265
|
+
* return-union (whose `|nil` sits at depth 0 after the `)`) is recognized as the
|
|
266
|
+
* function's own return, not an outer nullable — only a `|nil` applied to the whole
|
|
267
|
+
* token flags the param. Self-contained: the parser is upstream of the mapper and
|
|
268
|
+
* must not import it.
|
|
269
|
+
*/
|
|
270
|
+
function hasTopLevelNil(rawToken: string): boolean {
|
|
271
|
+
let token = rawToken.trim();
|
|
272
|
+
while (token.startsWith("(") && matchCloser(token, 0) === token.length - 1) {
|
|
273
|
+
token = token.slice(1, -1).trim();
|
|
274
|
+
}
|
|
275
|
+
if (/^fun\s*\(/.test(token)) {
|
|
276
|
+
const close = matchCloser(token, token.indexOf("("));
|
|
277
|
+
if (
|
|
278
|
+
close !== -1 &&
|
|
279
|
+
token
|
|
280
|
+
.slice(close + 1)
|
|
281
|
+
.trim()
|
|
282
|
+
.startsWith(":")
|
|
283
|
+
)
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
// A type-suffix `T?` (`string?`, `fun()?`, `(a|b)?`) is nil-bearing. Placed after the
|
|
287
|
+
// fun-return guard so a nullable return (`fun(): a|nil`, `fun(): string?`) never flags.
|
|
288
|
+
if (token.endsWith("?")) return true;
|
|
289
|
+
const isNilSeg = (from: number, to: number): boolean => token.slice(from, to).trim() === "nil";
|
|
290
|
+
let depth = 0;
|
|
291
|
+
let inQuote = false;
|
|
292
|
+
let segStart = 0;
|
|
293
|
+
for (let i = 0; i < token.length; i++) {
|
|
294
|
+
const c = token[i];
|
|
295
|
+
if (inQuote) {
|
|
296
|
+
if (c === '"') inQuote = false;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (c === '"') inQuote = true;
|
|
300
|
+
else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
301
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
302
|
+
else if (depth === 0 && c === "|") {
|
|
303
|
+
if (isNilSeg(segStart, i)) return true;
|
|
304
|
+
segStart = i + 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return isNilSeg(segStart, token.length);
|
|
308
|
+
}
|
|
309
|
+
|
|
104
310
|
/** A bare lowercase identifier — the shape druid uses for an optional `@return` name. */
|
|
105
311
|
const RETURN_NAME = /^[a-z_][A-Za-z0-9_]*$/;
|
|
106
312
|
|
|
@@ -112,10 +318,19 @@ function parseParam(rest: string): LibraryParam {
|
|
|
112
318
|
const isOptional = !isVararg && rawName.endsWith("?");
|
|
113
319
|
const name = isOptional ? rawName.slice(0, -1) : rawName;
|
|
114
320
|
const { type, rest: doc } = readTypeToken(afterName);
|
|
115
|
-
|
|
321
|
+
const param: LibraryParam = { name, types: type ? [type] : [], doc, isOptional, isVararg };
|
|
322
|
+
if (type && hasTopLevelNil(type)) param.isNilable = true;
|
|
323
|
+
return param;
|
|
116
324
|
}
|
|
117
325
|
|
|
118
|
-
|
|
326
|
+
/**
|
|
327
|
+
* A single `---@return` line may declare several values (`---@return number, number`),
|
|
328
|
+
* which `readTypeToken`'s comma continuation hands back as one token. Split it on
|
|
329
|
+
* top-level commas into one entry per value; LuaLS attaches the optional trailing name
|
|
330
|
+
* and description to the whole line, so only the last segment carries them. The
|
|
331
|
+
* emitter's `renderReturn` already lowers a multi-entry `returns` to `LuaMultiReturn`.
|
|
332
|
+
*/
|
|
333
|
+
function parseReturn(rest: string): LibraryParam[] {
|
|
119
334
|
const { type, rest: afterType } = readTypeToken(rest);
|
|
120
335
|
const spaceAt = afterType.search(/\s/);
|
|
121
336
|
const head = spaceAt === -1 ? afterType : afterType.slice(0, spaceAt);
|
|
@@ -125,7 +340,22 @@ function parseReturn(rest: string): LibraryParam {
|
|
|
125
340
|
name = head;
|
|
126
341
|
doc = spaceAt === -1 ? "" : afterType.slice(spaceAt).trim();
|
|
127
342
|
}
|
|
128
|
-
|
|
343
|
+
const segments = (type ? splitTopLevel(type, ",") : [""])
|
|
344
|
+
.map((segment) => segment.trim())
|
|
345
|
+
.filter((segment) => segment !== "");
|
|
346
|
+
if (segments.length === 0) {
|
|
347
|
+
return [{ name, types: [], doc, isOptional: false, isVararg: false }];
|
|
348
|
+
}
|
|
349
|
+
return segments.map((segment, index) => {
|
|
350
|
+
const isLast = index === segments.length - 1;
|
|
351
|
+
return {
|
|
352
|
+
name: isLast ? name : "",
|
|
353
|
+
types: [segment],
|
|
354
|
+
doc: isLast ? doc : "",
|
|
355
|
+
isOptional: false,
|
|
356
|
+
isVararg: false,
|
|
357
|
+
};
|
|
358
|
+
});
|
|
129
359
|
}
|
|
130
360
|
|
|
131
361
|
const VISIBILITY_KEYWORDS = new Set<LibraryFieldVisibility>([
|
|
@@ -152,9 +382,12 @@ function parseField(rest: string): LibraryField {
|
|
|
152
382
|
const spaceAt = body.search(/\s/);
|
|
153
383
|
const rawName = spaceAt === -1 ? body : body.slice(0, spaceAt);
|
|
154
384
|
const afterName = spaceAt === -1 ? "" : body.slice(spaceAt).trim();
|
|
155
|
-
const
|
|
156
|
-
const name =
|
|
385
|
+
const nameSuffix = rawName.endsWith("?");
|
|
386
|
+
const name = nameSuffix ? rawName.slice(0, -1) : rawName;
|
|
157
387
|
const { type, rest: doc } = readTypeToken(afterName);
|
|
388
|
+
// Interface properties carry no optional-before-required rule, so a `|nil`/`?` type
|
|
389
|
+
// folds straight into the field's optionality.
|
|
390
|
+
const isOptional = nameSuffix || (type !== "" && hasTopLevelNil(type));
|
|
158
391
|
return {
|
|
159
392
|
name,
|
|
160
393
|
types: type ? [type] : [],
|
|
@@ -166,7 +399,15 @@ function parseField(rest: string): LibraryField {
|
|
|
166
399
|
|
|
167
400
|
function parseVararg(rest: string): LibraryParam {
|
|
168
401
|
const { type, rest: doc } = readTypeToken(rest);
|
|
169
|
-
|
|
402
|
+
const param: LibraryParam = {
|
|
403
|
+
name: "...",
|
|
404
|
+
types: type ? [type] : [],
|
|
405
|
+
doc,
|
|
406
|
+
isOptional: false,
|
|
407
|
+
isVararg: true,
|
|
408
|
+
};
|
|
409
|
+
if (type && hasTopLevelNil(type)) param.isNilable = true;
|
|
410
|
+
return param;
|
|
170
411
|
}
|
|
171
412
|
|
|
172
413
|
function parseGenerics(rest: string): LibraryGeneric[] {
|
|
@@ -181,12 +422,50 @@ function parseGenerics(rest: string): LibraryGeneric[] {
|
|
|
181
422
|
});
|
|
182
423
|
}
|
|
183
424
|
|
|
184
|
-
/**
|
|
425
|
+
/**
|
|
426
|
+
* Read one class-head token from `s` starting at `start`, tracking `<[({` depth so a
|
|
427
|
+
* generic like `Bar<A, B>` stays whole. Ends at the first top-level space/tab, `:`, or
|
|
428
|
+
* `,` (none consumed). Returns the trimmed token and its end index. Distinct from
|
|
429
|
+
* `readTypeToken`, whose `fun(...)` `:`/`,` continuation rule wrongly glues a
|
|
430
|
+
* `Name: Parent` head together.
|
|
431
|
+
*/
|
|
432
|
+
function readClassToken(s: string, start: number): { token: string; end: number } {
|
|
433
|
+
let depth = 0;
|
|
434
|
+
let i = start;
|
|
435
|
+
for (; i < s.length; i++) {
|
|
436
|
+
const c = s[i];
|
|
437
|
+
if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
438
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
439
|
+
else if (depth === 0 && (c === " " || c === "\t" || c === ":" || c === ",")) break;
|
|
440
|
+
}
|
|
441
|
+
return { token: s.slice(start, i).trim(), end: i };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Parse a `@class Name[: parent[, parent...]] [description]` head. The identifier and
|
|
446
|
+
* optional parent list are read as bracket-aware tokens; any trailing human
|
|
447
|
+
* description is dropped. Multiple parents are joined with `", "` to match the single
|
|
448
|
+
* `extends` string the emitter renders.
|
|
449
|
+
*/
|
|
185
450
|
function parseClassHead(rest: string): { name: string; extends?: string } {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
451
|
+
const s = rest.trim();
|
|
452
|
+
const { token: name, end: afterName } = readClassToken(s, 0);
|
|
453
|
+
let i = afterName;
|
|
454
|
+
while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
|
|
455
|
+
if (s[i] !== ":") return { name };
|
|
456
|
+
i++;
|
|
457
|
+
const parents: string[] = [];
|
|
458
|
+
while (true) {
|
|
459
|
+
while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
|
|
460
|
+
const { token, end } = readClassToken(s, i);
|
|
461
|
+
if (token !== "") parents.push(token);
|
|
462
|
+
i = end;
|
|
463
|
+
while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
|
|
464
|
+
if (s[i] !== ",") break;
|
|
465
|
+
i++;
|
|
466
|
+
}
|
|
467
|
+
const extendsStr = parents.join(", ");
|
|
468
|
+
return { name, ...(extendsStr ? { extends: extendsStr } : {}) };
|
|
190
469
|
}
|
|
191
470
|
|
|
192
471
|
interface FunctionDecl {
|
|
@@ -260,6 +539,19 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
260
539
|
let pending = emptyPending();
|
|
261
540
|
let openClass: LibraryInterface | null = null;
|
|
262
541
|
let lastOpenedClass: string | null = null;
|
|
542
|
+
let moduleObject: string | undefined;
|
|
543
|
+
let lastModuleFunction: LibraryMethod | null = null;
|
|
544
|
+
// The one open function-local `---@class` block (squid's `SquidInstance` inside
|
|
545
|
+
// `Squid.new`). `owner` is the function whose body it sits in, so a returned local
|
|
546
|
+
// bound to it infers the function's return; `pendingType` is a `---@type fun(...)`
|
|
547
|
+
// armed for the next member key.
|
|
548
|
+
let localClass: {
|
|
549
|
+
iface: LibraryInterface;
|
|
550
|
+
localVar: string | null;
|
|
551
|
+
owner: LibraryMethod | null;
|
|
552
|
+
pendingType: string | null;
|
|
553
|
+
returnedSelf: boolean;
|
|
554
|
+
} | null = null;
|
|
263
555
|
|
|
264
556
|
const ensureInterface = (name: string): LibraryInterface => {
|
|
265
557
|
const existing = byName.get(name);
|
|
@@ -282,11 +574,94 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
282
574
|
generics: pending.generics,
|
|
283
575
|
params: pending.params,
|
|
284
576
|
returns: pending.returns,
|
|
577
|
+
...(pending.visibility ? { visibility: pending.visibility } : {}),
|
|
578
|
+
...(pending.deprecated !== undefined ? { deprecated: pending.deprecated } : {}),
|
|
285
579
|
});
|
|
286
580
|
|
|
581
|
+
// Interpret one indented line while a function-local `---@class` block is open. Only
|
|
582
|
+
// the `---@type fun(...)` + next `<key> = ...` member pattern, the backing `local`,
|
|
583
|
+
// and the `return <local>` are recognized; every other indented line stays opaque.
|
|
584
|
+
const handleLocalClassLine = (line: string): void => {
|
|
585
|
+
const lc = localClass;
|
|
586
|
+
if (!lc) return;
|
|
587
|
+
const typeMatch = /^---@type\s+(.+)$/.exec(line);
|
|
588
|
+
if (typeMatch) {
|
|
589
|
+
const { type } = readTypeToken(typeMatch[1] ?? "");
|
|
590
|
+
// A non-`fun` `---@type` also disarms a previously-armed one.
|
|
591
|
+
lc.pendingType = /^fun\s*\(/.test(type) ? type : null;
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (lc.pendingType) {
|
|
595
|
+
const keyMatch = /^([A-Za-z_]\w*)\s*=/.exec(line);
|
|
596
|
+
if (keyMatch) {
|
|
597
|
+
const { params, returns } = funToMethodParts(lc.pendingType);
|
|
598
|
+
lc.iface.methods.push({
|
|
599
|
+
name: keyMatch[1] as string,
|
|
600
|
+
brief: "",
|
|
601
|
+
generics: [],
|
|
602
|
+
params,
|
|
603
|
+
returns,
|
|
604
|
+
});
|
|
605
|
+
lc.pendingType = null;
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// Not the immediately-following member key: the armed type has no member.
|
|
609
|
+
lc.pendingType = null;
|
|
610
|
+
}
|
|
611
|
+
const returnMatch = /^return\s+([A-Za-z_]\w*)\s*$/.exec(line);
|
|
612
|
+
if (returnMatch) {
|
|
613
|
+
if (returnMatch[1] === lc.localVar) lc.returnedSelf = true;
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const local = LOCAL_ASSIGN.exec(line);
|
|
617
|
+
if (local) {
|
|
618
|
+
// A `---@class` annotates the immediately-following declaration, so only the
|
|
619
|
+
// first local after the class opens is the instance; a later local must not rebind.
|
|
620
|
+
if (lc.localVar === null) lc.localVar = local[1] ?? null;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
287
625
|
for (const raw of source.split("\n")) {
|
|
288
|
-
|
|
289
|
-
if (
|
|
626
|
+
const indented = /^\s/.test(raw);
|
|
627
|
+
if (indented || raw.length === 0) {
|
|
628
|
+
if (localClass) {
|
|
629
|
+
handleLocalClassLine(raw.trim());
|
|
630
|
+
} else if (indented) {
|
|
631
|
+
// Only an indented `---@class` opens function-local capture; every other
|
|
632
|
+
// indented line (druid-style `---@cast`/`---@type` narrowing) stays opaque.
|
|
633
|
+
const classMatch = /^---@class\s+(.+)$/.exec(raw.trim());
|
|
634
|
+
if (classMatch) {
|
|
635
|
+
const head = parseClassHead(classMatch[1] ?? "");
|
|
636
|
+
const iface = ensureInterface(head.name);
|
|
637
|
+
if (head.extends) iface.extends = head.extends;
|
|
638
|
+
localClass = {
|
|
639
|
+
iface,
|
|
640
|
+
localVar: null,
|
|
641
|
+
owner: lastModuleFunction,
|
|
642
|
+
pendingType: null,
|
|
643
|
+
returnedSelf: false,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// A column-0 line ends any open function-local class (dedent). Apply the inferred
|
|
651
|
+
// return to the owner only when it declared no explicit `---@return`.
|
|
652
|
+
if (localClass) {
|
|
653
|
+
const lc = localClass;
|
|
654
|
+
if (lc.returnedSelf && lc.owner && lc.owner.returns.length === 0) {
|
|
655
|
+
lc.owner.returns.push({
|
|
656
|
+
name: "",
|
|
657
|
+
types: [lc.iface.name],
|
|
658
|
+
doc: "",
|
|
659
|
+
isOptional: false,
|
|
660
|
+
isVararg: false,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
localClass = null;
|
|
664
|
+
}
|
|
290
665
|
|
|
291
666
|
if (raw.startsWith("---@")) {
|
|
292
667
|
const tagMatch = /^---@([a-zA-Z]+)\s*(.*)$/.exec(raw);
|
|
@@ -300,6 +675,8 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
300
675
|
if (head.extends) iface.extends = head.extends;
|
|
301
676
|
if (pending.doc.length > 0 && iface.brief === "") iface.brief = pending.doc.join("\n");
|
|
302
677
|
if (pending.generics.length > 0) iface.generics = pending.generics;
|
|
678
|
+
if (pending.overloads.length > 0) iface.overloads = pending.overloads;
|
|
679
|
+
if (pending.deprecated !== undefined) iface.deprecated = pending.deprecated;
|
|
303
680
|
openClass = iface;
|
|
304
681
|
lastOpenedClass = head.name;
|
|
305
682
|
pending = emptyPending();
|
|
@@ -318,24 +695,54 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
318
695
|
break;
|
|
319
696
|
}
|
|
320
697
|
case "return": {
|
|
321
|
-
pending.returns.push(parseReturn(rest));
|
|
698
|
+
pending.returns.push(...parseReturn(rest));
|
|
322
699
|
break;
|
|
323
700
|
}
|
|
324
701
|
case "generic": {
|
|
325
702
|
pending.generics.push(...parseGenerics(rest));
|
|
326
703
|
break;
|
|
327
704
|
}
|
|
705
|
+
case "overload": {
|
|
706
|
+
// A class-level `---@overload fun(...)`: keep the raw `fun(...)` token via
|
|
707
|
+
// readTypeToken (its spaced `): ret` return stays whole) plus the trailing
|
|
708
|
+
// doc, and transfer to the interface on the following `@class` — like brief
|
|
709
|
+
// and generics. A non-`fun` overload is outside the modeled subset and dropped.
|
|
710
|
+
const { type, rest: doc } = readTypeToken(rest);
|
|
711
|
+
if (/^fun\s*\(/.test(type)) pending.overloads.push({ type, doc });
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
328
714
|
case "alias": {
|
|
329
715
|
const spaceAt = rest.search(/\s/);
|
|
330
716
|
const name = spaceAt === -1 ? rest : rest.slice(0, spaceAt);
|
|
331
717
|
const expr = spaceAt === -1 ? "" : rest.slice(spaceAt).trim();
|
|
332
|
-
aliases.push({
|
|
718
|
+
aliases.push({
|
|
719
|
+
name,
|
|
720
|
+
types: expr ? [expr] : [],
|
|
721
|
+
doc: pending.doc.join("\n"),
|
|
722
|
+
...(pending.deprecated !== undefined ? { deprecated: pending.deprecated } : {}),
|
|
723
|
+
});
|
|
333
724
|
pending = emptyPending();
|
|
334
725
|
break;
|
|
335
726
|
}
|
|
727
|
+
case "private":
|
|
728
|
+
case "protected":
|
|
729
|
+
case "package":
|
|
730
|
+
case "local": {
|
|
731
|
+
// A standalone visibility tag on the pending block marks the next function
|
|
732
|
+
// declaration's method/module-function `visibility` (a `@class` or `@alias`
|
|
733
|
+
// that consumes the block first resets it, so it never leaks onto a type).
|
|
734
|
+
pending.visibility = tag as LibraryMethodVisibility;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
case "deprecated": {
|
|
738
|
+
// `rest` is already trimmed and is `""` for the bare form, which is the
|
|
739
|
+
// only form the fixtures actually ship; the text form is carried too.
|
|
740
|
+
pending.deprecated = rest;
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
336
743
|
default:
|
|
337
|
-
// @
|
|
338
|
-
//
|
|
744
|
+
// @cast, @type, @diagnostic, ... — outside the Druid subset; recognized as a
|
|
745
|
+
// tag and skipped, never treated as doc.
|
|
339
746
|
break;
|
|
340
747
|
}
|
|
341
748
|
continue;
|
|
@@ -348,12 +755,16 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
348
755
|
|
|
349
756
|
const decl = parseFunctionDecl(raw);
|
|
350
757
|
if (decl) {
|
|
758
|
+
const method = methodFromPending(decl.name);
|
|
351
759
|
if (decl.kind === "method") {
|
|
352
760
|
const target = decl.receiver ? (receiverBinding.get(decl.receiver) ?? decl.receiver) : "";
|
|
353
|
-
ensureInterface(target).methods.push(
|
|
761
|
+
ensureInterface(target).methods.push(method);
|
|
354
762
|
} else if (decl.qualified) {
|
|
355
|
-
moduleFunctions.push(
|
|
763
|
+
moduleFunctions.push(method);
|
|
356
764
|
}
|
|
765
|
+
// Any function may host a function-local `---@class`; remember the enclosing
|
|
766
|
+
// function so a returned local can infer its return, even a bare helper.
|
|
767
|
+
lastModuleFunction = method;
|
|
357
768
|
pending = emptyPending();
|
|
358
769
|
openClass = null;
|
|
359
770
|
continue;
|
|
@@ -366,10 +777,34 @@ export function parseLualsSource(source: string): LibraryModel {
|
|
|
366
777
|
lastOpenedClass = null;
|
|
367
778
|
openClass = null;
|
|
368
779
|
pending = emptyPending();
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// A column-0 `return <name>` handing back a local bound to an opened `---@class`
|
|
784
|
+
// marks that class as the module object (its fields become module-level consts).
|
|
785
|
+
// Restricted to squid's constants-table idiom to keep every other library's golden
|
|
786
|
+
// untouched: the returned local is named after its class (`local Squid = {}` /
|
|
787
|
+
// `return Squid`, not a generic `local M` alias), the class carries at least one
|
|
788
|
+
// public field, and it has no methods. A component/instance class (colon methods),
|
|
789
|
+
// an opaque handle (no fields), or an `M`-aliased module table is a type consumers
|
|
790
|
+
// reference or a plain namespace, so it stays a standalone interface.
|
|
791
|
+
const returnStmt = /^return\s+([A-Za-z_][\w.]*)\s*$/.exec(raw);
|
|
792
|
+
if (returnStmt) {
|
|
793
|
+
const name = returnStmt[1] as string;
|
|
794
|
+
const resolvedName = receiverBinding.get(name) ?? (byName.has(name) ? name : undefined);
|
|
795
|
+
const resolved = resolvedName ? byName.get(resolvedName) : undefined;
|
|
796
|
+
if (
|
|
797
|
+
resolved &&
|
|
798
|
+
resolvedName === name &&
|
|
799
|
+
resolved.methods.length === 0 &&
|
|
800
|
+
resolved.fields.some((f) => f.visibility === undefined || f.visibility === "public")
|
|
801
|
+
) {
|
|
802
|
+
moduleObject = resolved.name;
|
|
803
|
+
}
|
|
369
804
|
}
|
|
370
805
|
}
|
|
371
806
|
|
|
372
|
-
return { interfaces, aliases, moduleFunctions };
|
|
807
|
+
return { interfaces, aliases, moduleFunctions, ...(moduleObject ? { moduleObject } : {}) };
|
|
373
808
|
}
|
|
374
809
|
|
|
375
810
|
/**
|
|
@@ -383,8 +818,10 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
|
383
818
|
const byName = new Map<string, LibraryInterface>();
|
|
384
819
|
const aliases: LibraryAlias[] = [];
|
|
385
820
|
const moduleFunctions: LibraryMethod[] = [];
|
|
821
|
+
let moduleObject: string | undefined;
|
|
386
822
|
|
|
387
823
|
for (const model of models) {
|
|
824
|
+
if (!moduleObject && model.moduleObject) moduleObject = model.moduleObject;
|
|
388
825
|
for (const iface of model.interfaces) {
|
|
389
826
|
const existing = byName.get(iface.name);
|
|
390
827
|
if (!existing) {
|
|
@@ -395,6 +832,10 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
|
395
832
|
fields: [...iface.fields],
|
|
396
833
|
methods: [...iface.methods],
|
|
397
834
|
brief: iface.brief,
|
|
835
|
+
...(iface.overloads && iface.overloads.length > 0
|
|
836
|
+
? { overloads: [...iface.overloads] }
|
|
837
|
+
: {}),
|
|
838
|
+
...(iface.deprecated !== undefined ? { deprecated: iface.deprecated } : {}),
|
|
398
839
|
};
|
|
399
840
|
byName.set(iface.name, copy);
|
|
400
841
|
interfaces.push(copy);
|
|
@@ -403,10 +844,20 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
|
403
844
|
existing.fields.push(...iface.fields);
|
|
404
845
|
existing.methods.push(...iface.methods);
|
|
405
846
|
if (!existing.extends && iface.extends) existing.extends = iface.extends;
|
|
847
|
+
if (existing.deprecated === undefined && iface.deprecated !== undefined) {
|
|
848
|
+
existing.deprecated = iface.deprecated;
|
|
849
|
+
}
|
|
406
850
|
if (existing.brief === "" && iface.brief !== "") existing.brief = iface.brief;
|
|
407
851
|
if (existing.generics.length === 0 && iface.generics.length > 0) {
|
|
408
852
|
existing.generics = [...iface.generics];
|
|
409
853
|
}
|
|
854
|
+
if (
|
|
855
|
+
(!existing.overloads || existing.overloads.length === 0) &&
|
|
856
|
+
iface.overloads &&
|
|
857
|
+
iface.overloads.length > 0
|
|
858
|
+
) {
|
|
859
|
+
existing.overloads = [...iface.overloads];
|
|
860
|
+
}
|
|
410
861
|
}
|
|
411
862
|
aliases.push(...model.aliases);
|
|
412
863
|
moduleFunctions.push(...model.moduleFunctions);
|
|
@@ -419,7 +870,7 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
|
419
870
|
// overloaded module functions keep every signature.
|
|
420
871
|
for (const iface of interfaces) iface.fields = dedupeByName(iface.fields);
|
|
421
872
|
|
|
422
|
-
return { interfaces, aliases, moduleFunctions };
|
|
873
|
+
return { interfaces, aliases, moduleFunctions, ...(moduleObject ? { moduleObject } : {}) };
|
|
423
874
|
}
|
|
424
875
|
|
|
425
876
|
function dedupeByName<T extends { name: string }>(items: T[]): T[] {
|