@defold-typescript/library-types 0.20.7 → 0.21.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/decore.json +1883 -0
- package/api-doc/druid.json +11818 -0
- package/generated/decore.d.ts +228 -0
- package/generated/druid.d.ts +2067 -0
- package/luals-targets.json +28 -0
- package/package.json +8 -3
- package/scripts/__snapshots__/emit-library-dts.test.ts.snap +22 -0
- package/scripts/__snapshots__/parse-luals.test.ts.snap +15780 -0
- package/scripts/emit-library-dts.ts +252 -0
- package/scripts/lower-api-doc.ts +115 -0
- package/scripts/luals-fidelity.ts +123 -0
- package/scripts/map-luals-types.ts +323 -0
- package/scripts/parse-luals.ts +432 -0
- package/scripts/sync-luals-types.ts +303 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a `LibraryModel` (from `parse-luals.ts`) into a committed `.d.ts`: a
|
|
3
|
+
* single `declare module '<moduleId>' { ... }` block of plain `interface`s, `type`
|
|
4
|
+
* aliases, and `export function`s, mapping every member type through the shipped
|
|
5
|
+
* `mapLualsType`. Pure and deterministic — output depends only on the model and the
|
|
6
|
+
* options — so the `--emit` CLI arm and its golden round-trip test agree
|
|
7
|
+
* byte-for-byte.
|
|
8
|
+
*
|
|
9
|
+
* A sibling to `@defold-typescript/types`' `emit-dts.ts`, not an extension of it:
|
|
10
|
+
* that emitter is `ApiModule`-shaped and Defold-specific, whereas `LibraryModel` is
|
|
11
|
+
* a different OOP shape. Only `renderDocComment`, `TS_RESERVED_NAMES`, and
|
|
12
|
+
* `TS_IDENTIFIER` are reused from the types surface.
|
|
13
|
+
*
|
|
14
|
+
* Renders interface/method generic parameters and `extends` clauses: each generic
|
|
15
|
+
* `name` is scoped into a child `MapContext` (an identity rename) so a bare `T`
|
|
16
|
+
* resolves to `T` instead of lowering to `unknown`, constraints and `extends`
|
|
17
|
+
* targets map through the existing rename map, and an `extends` clause is emitted
|
|
18
|
+
* only for parents that resolve to a declared interface.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
luaMultiReturn,
|
|
23
|
+
renderDocComment,
|
|
24
|
+
TS_IDENTIFIER,
|
|
25
|
+
TS_RESERVED_NAMES,
|
|
26
|
+
varargElementType,
|
|
27
|
+
} from "@defold-typescript/types";
|
|
28
|
+
import {
|
|
29
|
+
type MapContext,
|
|
30
|
+
mapLualsType,
|
|
31
|
+
matchSelfHookField,
|
|
32
|
+
scopeGenerics,
|
|
33
|
+
} from "./map-luals-types";
|
|
34
|
+
import type {
|
|
35
|
+
LibraryAlias,
|
|
36
|
+
LibraryGeneric,
|
|
37
|
+
LibraryInterface,
|
|
38
|
+
LibraryMethod,
|
|
39
|
+
LibraryModel,
|
|
40
|
+
LibraryParam,
|
|
41
|
+
} from "./parse-luals";
|
|
42
|
+
|
|
43
|
+
export interface EmitLibraryOptions {
|
|
44
|
+
moduleId: string;
|
|
45
|
+
typeRenames?: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const INDENT = "\t";
|
|
49
|
+
|
|
50
|
+
/** A model type name (dotted like `druid.button`) reduced to a legal TS identifier. */
|
|
51
|
+
export function sanitizeTypeName(name: string): string {
|
|
52
|
+
const cleaned = name.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
53
|
+
return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An interface/type member key: bare when it is a plain identifier that is not a
|
|
58
|
+
* reserved word, otherwise a quoted string literal so a name like `default` or
|
|
59
|
+
* `new` stays an ordinary member instead of becoming a syntax error (or, for
|
|
60
|
+
* `new`, a construct signature).
|
|
61
|
+
*/
|
|
62
|
+
function memberKey(name: string): string {
|
|
63
|
+
return TS_IDENTIFIER.test(name) && !TS_RESERVED_NAMES.has(name) ? name : JSON.stringify(name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A parameter name coerced to a legal, non-reserved binding form. */
|
|
67
|
+
function safeParamName(name: string, index: number): string {
|
|
68
|
+
if (!TS_IDENTIFIER.test(name)) return `arg${index}`;
|
|
69
|
+
return TS_RESERVED_NAMES.has(name) ? `${name}_` : name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function mapTypes(types: readonly string[], ctx: MapContext): string {
|
|
73
|
+
if (types.length === 0) return "unknown";
|
|
74
|
+
return types.map((token) => mapLualsType(token, ctx).ts).join(" | ");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A generic parameter list `<A extends C, B>`, or `""` when there are none. */
|
|
78
|
+
export function renderGenericParams(generics: readonly LibraryGeneric[], ctx: MapContext): string {
|
|
79
|
+
if (generics.length === 0) return "";
|
|
80
|
+
const params = generics.map((generic) => {
|
|
81
|
+
if (generic.constraint === undefined || generic.constraint === "") return generic.name;
|
|
82
|
+
const mapped = mapTypes([generic.constraint], ctx);
|
|
83
|
+
// An undeclared constraint lowers to `unknown`; drop it (mirror `renderExtends`'s
|
|
84
|
+
// declared-only filter) rather than emit a vacuous `<T extends unknown>`.
|
|
85
|
+
return mapped === "unknown" ? generic.name : `${generic.name} extends ${mapped}`;
|
|
86
|
+
});
|
|
87
|
+
return `<${params.join(", ")}>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The TS return type for a self-hook field lowered to an optional method. */
|
|
91
|
+
function renderHookReturn(returnTokens: readonly string[], ctx: MapContext): string {
|
|
92
|
+
if (returnTokens.length === 0) return "void";
|
|
93
|
+
if (returnTokens.length === 1) return mapTypes([returnTokens[0] as string], ctx);
|
|
94
|
+
return luaMultiReturn(returnTokens.map((token) => mapTypes([token], ctx)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* An ` extends X, Y` clause built from `iface.extends` split on commas, keeping only
|
|
99
|
+
* parents that name a declared interface (so no `extends unknown` is ever emitted),
|
|
100
|
+
* mapped through the rename map; `""` when none survive.
|
|
101
|
+
*/
|
|
102
|
+
function renderExtends(
|
|
103
|
+
iface: LibraryInterface,
|
|
104
|
+
ctx: MapContext,
|
|
105
|
+
interfaceNames: ReadonlySet<string>,
|
|
106
|
+
): string {
|
|
107
|
+
if (!iface.extends) return "";
|
|
108
|
+
const parents = iface.extends
|
|
109
|
+
.split(",")
|
|
110
|
+
.map((name) => name.trim())
|
|
111
|
+
.filter((name) => interfaceNames.has(name))
|
|
112
|
+
.map((name) => mapTypes([name], ctx));
|
|
113
|
+
return parents.length > 0 ? ` extends ${parents.join(", ")}` : "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderParams(params: readonly LibraryParam[], ctx: MapContext): string {
|
|
117
|
+
return params
|
|
118
|
+
.map((param, index) => {
|
|
119
|
+
const mapped = mapTypes(param.types, ctx);
|
|
120
|
+
if (param.isVararg) {
|
|
121
|
+
return `...args: ${varargElementType(mapped)}`;
|
|
122
|
+
}
|
|
123
|
+
const optional = param.isOptional ? "?" : "";
|
|
124
|
+
return `${safeParamName(param.name, index)}${optional}: ${mapped}`;
|
|
125
|
+
})
|
|
126
|
+
.join(", ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderReturn(returns: readonly LibraryParam[], ctx: MapContext): string {
|
|
130
|
+
if (returns.length === 0) return "void";
|
|
131
|
+
if (returns.length === 1) return mapTypes((returns[0] as LibraryParam).types, ctx);
|
|
132
|
+
return luaMultiReturn(returns.map((ret) => mapTypes(ret.types, ctx)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pushDoc(lines: string[], summary: string, indent: string): void {
|
|
136
|
+
for (const line of renderDocComment({ summary })) lines.push(`${indent}${line}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderAlias(alias: LibraryAlias, ctx: MapContext): string[] {
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
pushDoc(lines, alias.doc, INDENT);
|
|
142
|
+
lines.push(`${INDENT}type ${sanitizeTypeName(alias.name)} = ${mapTypes(alias.types, ctx)};`);
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderInterface(
|
|
147
|
+
iface: LibraryInterface,
|
|
148
|
+
ctx: MapContext,
|
|
149
|
+
interfaceNames: ReadonlySet<string>,
|
|
150
|
+
): string[] {
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
pushDoc(lines, iface.brief, INDENT);
|
|
153
|
+
const ifaceCtx = scopeGenerics(ctx, iface.generics);
|
|
154
|
+
const params = renderGenericParams(iface.generics, ifaceCtx);
|
|
155
|
+
const extendsClause = renderExtends(iface, ifaceCtx, interfaceNames);
|
|
156
|
+
lines.push(`${INDENT}interface ${sanitizeTypeName(iface.name)}${params}${extendsClause} {`);
|
|
157
|
+
const body = INDENT + INDENT;
|
|
158
|
+
for (const field of iface.fields) {
|
|
159
|
+
// A base's self-receiving lifecycle hook is emitted as a permissive optional
|
|
160
|
+
// method (`name?(...args: any[]): ret`) so a concrete subinterface's refined
|
|
161
|
+
// override stays assignable under `extends`; strict function-field variance
|
|
162
|
+
// would reject it.
|
|
163
|
+
const hookReturns = matchSelfHookField(field.types, iface.name);
|
|
164
|
+
if (hookReturns !== null) {
|
|
165
|
+
pushDoc(lines, field.doc, body);
|
|
166
|
+
lines.push(
|
|
167
|
+
`${body}${memberKey(field.name)}?(...args: any[]): ${renderHookReturn(hookReturns, ifaceCtx)};`,
|
|
168
|
+
);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const optional = field.isOptional ? "?" : "";
|
|
172
|
+
lines.push(`${body}${memberKey(field.name)}${optional}: ${mapTypes(field.types, ifaceCtx)};`);
|
|
173
|
+
}
|
|
174
|
+
for (const method of iface.methods) {
|
|
175
|
+
pushDoc(lines, method.brief, body);
|
|
176
|
+
const methodCtx = scopeGenerics(ifaceCtx, method.generics);
|
|
177
|
+
const methodParams = renderGenericParams(method.generics, methodCtx);
|
|
178
|
+
lines.push(
|
|
179
|
+
`${body}${memberKey(method.name)}${methodParams}(${renderParams(
|
|
180
|
+
method.params,
|
|
181
|
+
methodCtx,
|
|
182
|
+
)}): ${renderReturn(method.returns, methodCtx)};`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
lines.push(`${INDENT}}`);
|
|
186
|
+
return lines;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderModuleFunction(fn: LibraryMethod, ctx: MapContext): string[] {
|
|
190
|
+
const lines: string[] = [];
|
|
191
|
+
pushDoc(lines, fn.brief, INDENT);
|
|
192
|
+
const fnCtx = scopeGenerics(ctx, fn.generics);
|
|
193
|
+
const genericParams = renderGenericParams(fn.generics, fnCtx);
|
|
194
|
+
const params = renderParams(fn.params, fnCtx);
|
|
195
|
+
const signature = `${genericParams}(${params ? `this: void, ${params}` : "this: void"}): ${renderReturn(
|
|
196
|
+
fn.returns,
|
|
197
|
+
fnCtx,
|
|
198
|
+
)}`;
|
|
199
|
+
const isReserved = TS_RESERVED_NAMES.has(fn.name) || !TS_IDENTIFIER.test(fn.name);
|
|
200
|
+
if (isReserved) {
|
|
201
|
+
// A reserved call name (`new`, `delete`) is illegal as a `function` identifier,
|
|
202
|
+
// so it is declared under an internal alias and re-exported under its real name.
|
|
203
|
+
const internal = `${sanitizeTypeName(fn.name)}_`;
|
|
204
|
+
lines.push(`${INDENT}export function ${internal}${signature};`);
|
|
205
|
+
lines.push(`${INDENT}export { ${internal} as ${fn.name} };`);
|
|
206
|
+
} else {
|
|
207
|
+
lines.push(`${INDENT}export function ${fn.name}${signature};`);
|
|
208
|
+
}
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The `MapContext` a library model maps every type token through: each interface
|
|
214
|
+
* and alias name is a known reference plus a sanitized rename (dotted `druid.button`
|
|
215
|
+
* -> `druid_button`), layered over the caller's `typeRenames`. Shared by the emitter
|
|
216
|
+
* (declaration text) and the api-doc lowering so both resolve model references
|
|
217
|
+
* identically — the invariant that keeps `api-doc/<ns>.json` byte-equivalent to
|
|
218
|
+
* `generated/<ns>.d.ts`.
|
|
219
|
+
*/
|
|
220
|
+
export function buildModelContext(
|
|
221
|
+
model: LibraryModel,
|
|
222
|
+
typeRenames?: Record<string, string>,
|
|
223
|
+
): MapContext {
|
|
224
|
+
const declaredNames = [
|
|
225
|
+
...model.interfaces.map((iface) => iface.name),
|
|
226
|
+
...model.aliases.map((alias) => alias.name),
|
|
227
|
+
];
|
|
228
|
+
const nameRenames: Record<string, string> = {};
|
|
229
|
+
for (const name of declaredNames) nameRenames[name] = sanitizeTypeName(name);
|
|
230
|
+
return {
|
|
231
|
+
knownNames: new Set(declaredNames),
|
|
232
|
+
typeRenames: { ...(typeRenames ?? {}), ...nameRenames },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Emit the `.d.ts` text for one library model. References to the model's own
|
|
238
|
+
* interfaces and aliases resolve to their sanitized declaration names via a rename
|
|
239
|
+
* map layered over the caller's `typeRenames`; unresolved references lower to
|
|
240
|
+
* `unknown` inside `mapLualsType` exactly as the fidelity report records them.
|
|
241
|
+
*/
|
|
242
|
+
export function emitLibraryDeclarations(model: LibraryModel, opts: EmitLibraryOptions): string {
|
|
243
|
+
const ctx = buildModelContext(model, opts.typeRenames);
|
|
244
|
+
|
|
245
|
+
const interfaceNames = new Set(model.interfaces.map((iface) => iface.name));
|
|
246
|
+
const out: string[] = ["/** @noResolution */", `declare module '${opts.moduleId}' {`];
|
|
247
|
+
for (const alias of model.aliases) out.push(...renderAlias(alias, ctx));
|
|
248
|
+
for (const iface of model.interfaces) out.push(...renderInterface(iface, ctx, interfaceNames));
|
|
249
|
+
for (const fn of model.moduleFunctions) out.push(...renderModuleFunction(fn, ctx));
|
|
250
|
+
out.push("}");
|
|
251
|
+
return `${out.join("\n")}\n`;
|
|
252
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lower a LuaLS `LibraryModel` (`parse-luals.ts`) to the `{ info, elements }`
|
|
3
|
+
* ref-doc JSON shape that `@defold-typescript/types`' `parseDefoldApiDoc`
|
|
4
|
+
* accepts, so the docs-site renders a LuaLS-sourced library through the exact
|
|
5
|
+
* `/api` path a ts-defold `.d.ts` takes. Mirrors `extract-api-doc.ts`: pure and
|
|
6
|
+
* node-free (model in, object out) so it is unit-testable and reused by the
|
|
7
|
+
* `--api-doc` orchestrator arm.
|
|
8
|
+
*
|
|
9
|
+
* We lower from the model rather than re-running `extractApiDoc` on the emitted
|
|
10
|
+
* `generated/<ns>.d.ts`: a druid-style library's surface is almost entirely
|
|
11
|
+
* `interface` declarations, and `extractApiDoc` only emits interfaces reachable
|
|
12
|
+
* from an emitted function/variable or an `export =`, so the emitted `.d.ts`
|
|
13
|
+
* yields near-empty elements. The model carries the interfaces explicitly.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
buildModelContext,
|
|
18
|
+
mapTypes,
|
|
19
|
+
renderGenericParams,
|
|
20
|
+
sanitizeTypeName,
|
|
21
|
+
} from "./emit-library-dts";
|
|
22
|
+
import { type MapContext, scopeGenerics } from "./map-luals-types";
|
|
23
|
+
import type { LibraryField, LibraryMethod, LibraryModel, LibraryParam } from "./parse-luals";
|
|
24
|
+
|
|
25
|
+
// A field with an explicit non-public visibility is internal surface; keep only
|
|
26
|
+
// fields with no visibility or an explicit `public`, mirroring how LuaLS hides
|
|
27
|
+
// `private`/`protected`/`package` members from a class's public shape.
|
|
28
|
+
function isPublicField(field: LibraryField): boolean {
|
|
29
|
+
return field.visibility === undefined || field.visibility === "public";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Each type token is mapped independently (one mapped TS string per token) so the
|
|
33
|
+
// ref-doc `types` array stays token-per-slot the way engine ref-docs are shaped.
|
|
34
|
+
function mapTokens(tokens: readonly string[], ctx: MapContext): string[] {
|
|
35
|
+
return tokens.map((token) => mapTypes([token], ctx));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parameterElement(param: LibraryParam, ctx: MapContext): Record<string, unknown> {
|
|
39
|
+
// A vararg's element type stays a plain mapped token here; the renderer arrayifies
|
|
40
|
+
// it (`...args: T[]`) from the `is_vararg` flag, keeping the JSON structurally honest.
|
|
41
|
+
return {
|
|
42
|
+
name: param.isVararg ? "...args" : param.name,
|
|
43
|
+
doc: param.doc,
|
|
44
|
+
types: mapTokens(param.types, ctx),
|
|
45
|
+
is_optional: param.isOptional ? "True" : "False",
|
|
46
|
+
is_vararg: param.isVararg ? "True" : "False",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function returnElement(ret: LibraryParam, ctx: MapContext): Record<string, unknown> {
|
|
51
|
+
return { name: "", doc: ret.doc, types: mapTokens(ret.types, ctx) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function functionElement(method: LibraryMethod, ctx: MapContext): Record<string, unknown> {
|
|
55
|
+
const fnCtx = scopeGenerics(ctx, method.generics);
|
|
56
|
+
const generics = renderGenericParams(method.generics, fnCtx);
|
|
57
|
+
return {
|
|
58
|
+
type: "FUNCTION",
|
|
59
|
+
name: method.name,
|
|
60
|
+
brief: method.brief,
|
|
61
|
+
description: method.brief,
|
|
62
|
+
...(generics !== "" ? { generics } : {}),
|
|
63
|
+
parameters: method.params.map((param) => parameterElement(param, fnCtx)),
|
|
64
|
+
returnvalues: method.returns.map((ret) => returnElement(ret, fnCtx)),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function propertyElement(field: LibraryField, ctx: MapContext): Record<string, unknown> {
|
|
69
|
+
return {
|
|
70
|
+
name: field.name,
|
|
71
|
+
brief: field.doc,
|
|
72
|
+
description: field.doc,
|
|
73
|
+
types: mapTokens(field.types, ctx),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function lowerLibraryModel(
|
|
78
|
+
model: LibraryModel,
|
|
79
|
+
{ namespace, typeRenames }: { namespace: string; typeRenames?: Record<string, string> },
|
|
80
|
+
): unknown {
|
|
81
|
+
const ctx = buildModelContext(model, typeRenames);
|
|
82
|
+
const elements: Record<string, unknown>[] = [];
|
|
83
|
+
|
|
84
|
+
for (const fn of model.moduleFunctions) {
|
|
85
|
+
elements.push(functionElement(fn, ctx));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const iface of model.interfaces) {
|
|
89
|
+
const ifaceCtx = scopeGenerics(ctx, iface.generics);
|
|
90
|
+
const functions = iface.methods.map((method) => functionElement(method, ifaceCtx));
|
|
91
|
+
const properties = iface.fields
|
|
92
|
+
.filter(isPublicField)
|
|
93
|
+
.map((field) => propertyElement(field, ifaceCtx));
|
|
94
|
+
elements.push({
|
|
95
|
+
type: "TYPEDEF",
|
|
96
|
+
name: sanitizeTypeName(iface.name),
|
|
97
|
+
...(functions.length > 0 ? { functions } : {}),
|
|
98
|
+
...(properties.length > 0 ? { properties } : {}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const alias of model.aliases) {
|
|
103
|
+
elements.push({ type: "TYPEDEF", name: sanitizeTypeName(alias.name) });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The module's own `@class` (named for the namespace, e.g. `@class druid`)
|
|
107
|
+
// carries the library's summary; use it as the page description so a
|
|
108
|
+
// LuaLS-sourced library reads with an intro like every other `/api` page,
|
|
109
|
+
// rather than opening on a bare provenance block. `brief` is its first line.
|
|
110
|
+
const moduleClass = model.interfaces.find((iface) => iface.name === namespace);
|
|
111
|
+
const description = moduleClass?.brief ?? "";
|
|
112
|
+
const brief = description.split("\n")[0] ?? "";
|
|
113
|
+
|
|
114
|
+
return { info: { namespace, brief, description }, elements };
|
|
115
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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, scopeGenerics } from "./map-luals-types";
|
|
14
|
+
import type { LibraryGeneric, 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[], mapCtx: MapContext): void => {
|
|
55
|
+
for (const token of tokens) {
|
|
56
|
+
totalTypeTokens++;
|
|
57
|
+
const { unknowns } = mapLualsType(token, mapCtx);
|
|
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
|
+
const mapConstraints = (generics: readonly LibraryGeneric[], mapCtx: MapContext): void => {
|
|
66
|
+
for (const generic of generics) {
|
|
67
|
+
if (generic.constraint) mapTokens([generic.constraint], mapCtx);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const iface of model.interfaces) {
|
|
72
|
+
const ifaceCtx = scopeGenerics(ctx, iface.generics);
|
|
73
|
+
if (iface.extends) {
|
|
74
|
+
mapTokens(
|
|
75
|
+
iface.extends
|
|
76
|
+
.split(",")
|
|
77
|
+
.map((parent) => parent.trim())
|
|
78
|
+
.filter((parent) => parent !== ""),
|
|
79
|
+
ifaceCtx,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
mapConstraints(iface.generics, ifaceCtx);
|
|
83
|
+
for (const field of iface.fields) {
|
|
84
|
+
totalMembers++;
|
|
85
|
+
if (undocumented(field.doc)) undocumentedMembers++;
|
|
86
|
+
mapTokens(field.types, ifaceCtx);
|
|
87
|
+
}
|
|
88
|
+
for (const method of iface.methods) {
|
|
89
|
+
totalMembers++;
|
|
90
|
+
if (undocumented(method.brief)) undocumentedMembers++;
|
|
91
|
+
const methodCtx = scopeGenerics(ifaceCtx, method.generics);
|
|
92
|
+
mapConstraints(method.generics, methodCtx);
|
|
93
|
+
for (const param of method.params) mapTokens(param.types, methodCtx);
|
|
94
|
+
for (const ret of method.returns) mapTokens(ret.types, methodCtx);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const fn of model.moduleFunctions) {
|
|
99
|
+
totalMembers++;
|
|
100
|
+
if (undocumented(fn.brief)) undocumentedMembers++;
|
|
101
|
+
const fnCtx = scopeGenerics(ctx, fn.generics);
|
|
102
|
+
mapConstraints(fn.generics, fnCtx);
|
|
103
|
+
for (const param of fn.params) mapTokens(param.types, fnCtx);
|
|
104
|
+
for (const ret of fn.returns) mapTokens(ret.types, fnCtx);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const alias of model.aliases) mapTokens(alias.types, ctx);
|
|
108
|
+
|
|
109
|
+
const coverage =
|
|
110
|
+
totalTypeTokens === 0
|
|
111
|
+
? 1
|
|
112
|
+
: round3(Math.max(0, Math.min(1, (totalTypeTokens - unknownFallbacks) / totalTypeTokens)));
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
namespace,
|
|
116
|
+
totalMembers,
|
|
117
|
+
totalTypeTokens,
|
|
118
|
+
unknownFallbacks,
|
|
119
|
+
unknownTokens: [...unknownTokens].sort(),
|
|
120
|
+
undocumentedMembers,
|
|
121
|
+
coverage,
|
|
122
|
+
};
|
|
123
|
+
}
|