@runtime-type-inspector/transpiler 3.0.5 → 3.0.7

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.
Files changed (3) hide show
  1. package/index.cjs +3005 -2968
  2. package/index.mjs +1420 -1391
  3. package/package.json +2 -2
package/index.mjs CHANGED
@@ -2,223 +2,612 @@ import { parse } from '@babel/parser';
2
2
  import ts from 'typescript';
3
3
 
4
4
  /**
5
- * @todo expandTypeDepFree doesn't support "complicated" types.
6
- * For actual builds, we use expandType() anyway (which is based on TypeScript).
7
- * But since TypeScript is a huge dependency, I'm looking into BabelFlow/BabelTypescript parser.
8
- * Comparing AST's like this usually helps to find bugs or potential issues,
9
- * while we can benchmark for best performance too.
10
- * @example
11
- * const tooComplex = 'Array<string|{chunks?: undefined|Array<{language: string|null, timestamp: Array<number|null>, text: string}>}>';
12
- * console.log(expandTypeDepFree(tooComplex));
13
- */
14
- /**
15
- * @typedef {object} ExpandTypeReturnValue
16
- * @property {'array' | 'union' | 'record' | 'tuple' | 'object' | 'promise' | 'typeof'} type - The type.
17
- * @property {object | string} [elementType] - For Array.
18
- * @property {object | string} [key] - For Record<key, val>
19
- * @property {object | string} [val] - For Record<key, val>
20
- * @property {(object | string)[]} [members] - For unions.
21
- * @property {object | string} [properties] - For objects.
22
- * @property {(object | string)[]} [elements] - For tuples.
23
- * @property {object | string} [argument] - For typeof.
24
- */
25
- /**
26
- * 'DepFree' refers to the fact that this function has no dependencies,
27
- * while `expandType` depends on TypeScript itself for maximum compatibility.
28
- * @example
29
- * expandTypeDepFree('(123) '); // Outputs: '123'
30
- * expandTypeDepFree('Array<number> '); // Outputs: { type: 'array', elementType: 'number' }
31
- * expandTypeDepFree('Array<(123) > '); // Outputs: { type: 'array', elementType: '123' }
32
- * expandTypeDepFree(' ( ( 123 ) ) '); // Outputs: '123'
33
- * expandTypeDepFree(' (string ) |(number ) '); // Outputs: { type: 'union', members: ['string', 'number'] }
34
- * expandTypeDepFree(' (( Object ) ) '); // Outputs: { type: 'object', properties: {} }
35
- * @param {string} type - The input type to expand.
36
- * @returns {string | ExpandTypeReturnValue} Object containing parsed information from type string.
37
- */
38
- function expandTypeDepFree(type) {
39
- type = type.trim();
40
- // '(123)' -> '123'
41
- while (!type.includes('|') && type[0] === '(' && type[type.length - 1] === ')') {
42
- type = type.slice(1, -1).trim();
43
- }
44
- // (1) Rest parameters like ...string
45
- if (type[0] === '.' && type[1] === '.' && type[2] === '.') {
46
- const elementType = type.slice(3);
47
- return {
48
- type: 'array',
49
- elementType
50
- };
51
- }
52
- // (2)
53
- // Array<...>
54
- if (type.startsWith("Array<") && type.endsWith('>')) {
55
- const typeSlice = type.slice(6, -1);
56
- const elementType = expandTypeDepFree(typeSlice);
57
- return {
58
- type: "array",
59
- elementType
60
- };
61
- }
62
- // Promise<...>
63
- if (type.startsWith("Promise<") && type.endsWith('>')) {
64
- const typeSlice = type.slice(8, -1);
65
- const elementType = expandTypeDepFree(typeSlice);
66
- return {
67
- type: "promise",
68
- elementType
69
- };
70
- }
71
- // (3) Object<...> or Record<...>
72
- if ((type.startsWith("Object<") || type.startsWith("Record<")) && type.endsWith('>')) {
73
- const recordSlice = type.slice(7, -1);
74
- const firstComma = recordSlice.indexOf(',');
75
- if (firstComma === -1) {
76
- console.warn("expandTypeDepFree> invalid Object/Record");
77
- }
78
- const key = recordSlice.slice(0, firstComma).trim();
79
- const val = recordSlice.slice(firstComma + 1).trim();
80
- return {
81
- type: "record",
82
- key: expandTypeDepFree(key),
83
- val: expandTypeDepFree(val)
84
- };
85
- }
86
- // (4) {...}
87
- if (type[0] === '{' && type[type.length - 1] === '}') {
88
- const propertiesArray = type.slice(1, -1).split(','); // ['entity: Entity', ' app: AppBase']
89
- const properties = {};
90
- propertiesArray.forEach(_ => {
91
- const [propName, propType] = _.split(":").map(_ => _.trim());
92
- if (!propName || !propType) {
93
- console.warn('expandTypeDepFree> unexpected type format, fix');
94
- return false;
95
- }
96
- properties[propName] = propType;
97
- });
98
- return {
99
- type: 'object',
100
- properties
101
- };
102
- }
103
- // (5) expand unions
104
- const members = type.split("|");
105
- if (members.length >= 2) {
106
- members.forEach((_, i) => members[i] = _.trim());
107
- return {
108
- type: 'union',
109
- members: members.map(expandTypeDepFree)
110
- };
111
- }
112
- // (6) expand [] Arrays
113
- // Test arrays: new pc.Mat3().set([1, 2, 3, "asd"])
114
- if (type.endsWith("[]")) {
115
- const typeSlice = type.slice(0, -2);
116
- return {
117
- type: 'array',
118
- elementType: expandTypeDepFree(typeSlice)
119
- };
120
- }
121
- // (7) expand tuples
122
- if (type[0] === '[' && type[type.length - 1] === ']') {
123
- const elements = type.slice(1, -1).split(','); // ['null', ' Texture', ' Texture', ' Texture', ' Texture', ' Texture', ' Texture']
124
- return {
125
- type: 'tuple',
126
- elements: elements.map(expandTypeDepFree)
127
- };
128
- }
129
- // (8) expand typeof expressions
130
- if (type.startsWith('typeof ')) {
131
- const argument = expandTypeDepFree(type.substring(7));
132
- return {
133
- type: 'typeof',
134
- argument
135
- };
136
- }
137
- if (type === 'object' || type === 'Object') {
138
- return {
139
- type: 'object',
140
- properties: {}
141
- };
142
- }
143
- return type;
144
- }
145
-
146
- /** @typedef {import('@babel/types').Node} Node */
147
- /** @typedef {import('@babel/types').Function} Function */
148
- /**
149
- * Checks if the provided node is a function-like structure.
5
+ * Transforms a type string into a structured type representation.
150
6
  *
151
- * @param {Node} node - The Babel AST node to be tested.
152
- * @returns {node is Function} - `true` if the node is a function-like structure, otherwise `false`.
7
+ * This function parses a given type string and converts it into a TypeScript
8
+ * Abstract Syntax Tree (AST), then uses that AST to return a structured type
9
+ * representation that can be further utilized or interpreted.
10
+ *
11
+ * @todo Better handling of weird case: Array<>
12
+ * @example
13
+ * const {expandType} = await import("./src-transpiler/expandType.mjs");
14
+ * expandType('[string, Array|AnyTypedArray, number[]]|[ONNXTensor]');
15
+ * expandType('(123) '); // Outputs: '123'
16
+ * expandType(' ( ( 123 ) ) '); // Outputs: '123'
17
+ * expandType('Array<number> '); // Outputs: {type: 'array', elementType: 'number'}
18
+ * expandType('Array<(123) > '); // Outputs: {type: 'array', elementType: '123'}
19
+ * expandType('Array<"abc" | 123> '); // Outputs: {type: 'array', elementType: {type: 'union', members: ['"abc"', '123']}}
20
+ * expandType(' (string ) |(number ) '); // Outputs: {type: 'union', members: [ 'string', 'number']}
21
+ * expandType(' "apples" | ( "bananas") '); // Outputs: {type: 'union', members: [ '"apples"', '"bananas"']}
22
+ * expandType('123? '); // Outputs: {"type":"union","members":["123","null"]}
23
+ * expandType('123|null '); // Outputs: {"type":"union","members":["123","null"]}
24
+ * expandType('Map<string, any> '); // Outputs:
25
+ * expandType('typeof Number '); // Outputs:
26
+ * @param {string} type - The type string to be expanded into a structured representation.
27
+ * @todo Share type with expandTypeBabelTS and expandTypeDepFree
28
+ * @returns {string | {type: string, [key: string]: any} | undefined} The structured type
29
+ * representation obtained from parsing and converting the provided type string.
153
30
  */
154
- function nodeIsFunction(node) {
155
- switch (node.type) {
156
- case 'ArrowFunctionExpression':
157
- case 'ClassMethod':
158
- case 'ClassPrivateMethod':
159
- case 'FunctionDeclaration':
160
- case 'FunctionExpression':
161
- case 'ObjectMethod':
162
- return true;
163
- }
164
- return false;
31
+ function expandType(type) {
32
+ const ast = parseType(type);
33
+ return toSourceTS(ast);
165
34
  }
166
-
167
35
  /**
168
- * @typedef DocType
169
- * @property {boolean} optional - Type is optional.
36
+ * @todo I want to use for example: import('typescript').Node
37
+ * But the TS types make no sense to me so far ... need to investigate more.
38
+ * @typedef TypeScriptType
39
+ * @property {object[]|undefined} typeArguments - The type arguments.
40
+ * @property {import('typescript').Node} typeName - The type name.
41
+ * @property {number} kind - The kind for `ts.SyntaxKind[kind]`.
170
42
  */
171
43
  /**
172
- * @param {string | DocType} type - The type.
173
- * @param {boolean} optional - Optionality
174
- * @returns {string | DocType} The simplified type.
44
+ * @param {string} str - The type string.
45
+ * @returns {TypeScriptType} - The node containing all the information about the input type string.
175
46
  */
176
- function simplifyType(type, optional) {
177
- // If it's already an object, just set optionality.
178
- if (type instanceof Object) {
179
- type.optional = optional;
180
- } else if (typeof type === 'string') {
181
- type = type.trim();
182
- if (type !== 'object' && type !== 'object[]' && type !== 'union' && !optional) {
183
- // console.log("simplify", type);
184
- return type;
185
- }
186
- type = {
187
- type,
188
- optional
189
- };
190
- } else {
191
- debugger;
192
- console.warn("simplifyType> neither object nor string for type", type);
193
- }
194
- if (type.type === 'object' && type.properties && Object.keys(type.properties).length === 0) {
195
- delete type.properties;
196
- // console.log("delete empty", type);
47
+ function parseType(str) {
48
+ // TS doesn't like ... notation in this context
49
+ if (str.startsWith('...')) {
50
+ str = str.slice(3); // remove dots
51
+ str += '[]'; // turn into array
197
52
  }
198
-
199
- return type;
53
+ // type tmp = (...string) => 123; to have a function context
54
+ str = `type tmp = ${str};`;
55
+ const ast = ts.createSourceFile('repl.ts', str, ts.ScriptTarget.Latest, true /*setParentNodes*/);
56
+ return ast.statements[0].type;
200
57
  }
201
-
58
+ /** @type {Record<string, 'missing'|'found'>} */
59
+ const requiredTypeofs = {};
202
60
  /**
203
- * Parses JSDoc comments to extract parameter type information.
61
+ * Converts a TypeScript AST node to a source string representation or to an intermediate object describing the type.
204
62
  *
205
- * @param {string} src - The JSDoc comment string to parse.
206
- * @param {Function} [expandType] - An optional function to process the types found in the JSDoc.
207
- * @returns {Record<string, any> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
63
+ * This function handles various TypeScript AST node types and converts them into a string
64
+ * or an object representing the type.
65
+ *
66
+ * @param {TypeScriptType} node - The TypeScript AST node to convert.
67
+ * @returns {string | number | boolean | {type: string, [key: string]: any} | undefined} The source string/number,
68
+ * or an object with type information based on the node, or `undefined` if the node kind is not handled.
208
69
  */
209
- function parseJSDoc(src, expandType = expandTypeDepFree) {
210
- // Parse something like: @param {Object} [kwargs={}] Optional arguments.
211
- const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\{\}\.'" ]+)/g;
212
- const matches = [...src.matchAll(regex)];
213
- /** @type {Record<string, any>} */
214
- const params = Object.create(null);
215
- matches.forEach(_ => {
216
- const type = expandType(_[1].trim());
217
- let name = _[2].trim();
218
- let optional = false;
219
- // Examples:
220
- // name: [kwargs={}] The configuration parameters.
221
- // name: [d = 1.0] Sample spacing
70
+ function toSourceTS(node) {
71
+ const {
72
+ typeArguments,
73
+ typeName
74
+ } = node;
75
+ const kind_ = ts.SyntaxKind[node.kind];
76
+ const {
77
+ AnyKeyword,
78
+ ArrayType,
79
+ BooleanKeyword,
80
+ FunctionType,
81
+ Identifier,
82
+ IntersectionType,
83
+ JSDocAllType,
84
+ LastTypeNode,
85
+ LiteralType,
86
+ NullKeyword,
87
+ NumberKeyword,
88
+ NumericLiteral,
89
+ ObjectKeyword,
90
+ Parameter,
91
+ ParenthesizedType,
92
+ PropertySignature,
93
+ StringKeyword,
94
+ StringLiteral,
95
+ ThisType,
96
+ TupleType,
97
+ TypeLiteral,
98
+ TypeReference,
99
+ UndefinedKeyword,
100
+ UnionType,
101
+ JSDocNullableType,
102
+ TrueKeyword,
103
+ FalseKeyword,
104
+ VoidKeyword,
105
+ UnknownKeyword,
106
+ NeverKeyword,
107
+ BigIntKeyword,
108
+ BigIntLiteral,
109
+ ConditionalType,
110
+ IndexedAccessType,
111
+ RestType,
112
+ TypeQuery,
113
+ // parseType('typeof Number')
114
+ TypeOperator,
115
+ // parseType('keyof typeof obj')
116
+ KeyOfKeyword,
117
+ // "operator" key in TypeOperator node
118
+ ConstructorType,
119
+ // parseType('new (...args: any[]) => any');
120
+ NamedTupleMember,
121
+ MappedType,
122
+ // parseType('{[K in TaskType]: InstanceType<typeof SUPPORTED_TASKS[K]["pipeline"]>}')
123
+ TypeParameter // Basically K and TaskType of MappedType
124
+ } = ts.SyntaxKind;
125
+ // console.log({typeArguments, typeName, kind_, node});
126
+ switch (node.kind) {
127
+ case BigIntKeyword:
128
+ return {
129
+ type: 'bigint'
130
+ };
131
+ case BigIntLiteral:
132
+ const literal = node.text.slice(0, -1); // Remove the "n"
133
+ return {
134
+ type: 'bigint',
135
+ literal
136
+ };
137
+ case ConditionalType:
138
+ // Keys on node:
139
+ // ['pos', 'end', 'flags', 'modifierFlagsCache', 'transformFlags', 'parent', 'kind', 'checkType',
140
+ // 'extendsType', 'trueType', 'falseType', 'locals', 'nextContainer']
141
+ const checkType = toSourceTS(node.checkType);
142
+ const extendsType = toSourceTS(node.extendsType);
143
+ const trueType = toSourceTS(node.trueType);
144
+ const falseType = toSourceTS(node.falseType);
145
+ return {
146
+ type: 'condition',
147
+ checkType,
148
+ extendsType,
149
+ trueType,
150
+ falseType
151
+ };
152
+ case ConstructorType:
153
+ {
154
+ const _parameters = node.parameters.map(toSourceTS);
155
+ const _ret = toSourceTS(node.type);
156
+ return {
157
+ type: 'new',
158
+ parameters: _parameters,
159
+ ret: _ret
160
+ };
161
+ }
162
+ case FunctionType:
163
+ const parameters = node.parameters.map(toSourceTS);
164
+ return {
165
+ type: 'function',
166
+ parameters
167
+ };
168
+ case IndexedAccessType:
169
+ const index = toSourceTS(node.indexType);
170
+ const object = toSourceTS(node.objectType);
171
+ return {
172
+ type: 'indexedAccess',
173
+ index,
174
+ object
175
+ };
176
+ case RestType:
177
+ const annotation = toSourceTS(node.type);
178
+ return {
179
+ type: 'rest',
180
+ annotation
181
+ };
182
+ case JSDocNullableType:
183
+ const t = toSourceTS(node.type);
184
+ return {
185
+ type: 'union',
186
+ members: [t, 'null']
187
+ };
188
+ case MappedType:
189
+ {
190
+ const result = toSourceTS(node.type);
191
+ const parameter = node.typeParameter;
192
+ if (parameter.kind === TypeParameter) {
193
+ // For example: {[K in TaskType]: InstanceType etc.
194
+ const iterable = toSourceTS(parameter.constraint); // TaskType
195
+ const element = toSourceTS(parameter.name); // K
196
+ return {
197
+ type: 'mapping',
198
+ iterable,
199
+ element,
200
+ result
201
+ };
202
+ }
203
+ console.warn("MappedType: expected TypeParameter");
204
+ return 'transpiler-error';
205
+ }
206
+ // todo work out more: const jsdoc = `(...a: ...number) => 123
207
+ // TS even thinks it's two parameters... just go for array/[]
208
+ case Parameter:
209
+ const type = node.type ? toSourceTS(node.type) : 'any';
210
+ const name = toSourceTS(node.name);
211
+ const ret = {
212
+ type,
213
+ name
214
+ };
215
+ if (node.dotDotDotToken) {
216
+ return {
217
+ type: 'array',
218
+ elementType: ret
219
+ };
220
+ }
221
+ return ret;
222
+ case TypeQuery:
223
+ const argument = toSourceTS(node.exprName);
224
+ // Notify Asserter class that we have to register variables with this name
225
+ if (!requiredTypeofs[argument]) {
226
+ requiredTypeofs[argument] = 'missing';
227
+ }
228
+ return {
229
+ type: 'typeof',
230
+ argument
231
+ };
232
+ case TypeOperator:
233
+ if (node.operator === KeyOfKeyword) {
234
+ const _argument = toSourceTS(node.type);
235
+ return {
236
+ type: 'keyof',
237
+ argument: _argument
238
+ };
239
+ }
240
+ console.warn("unimplemented TypeOperator", node);
241
+ case TypeReference:
242
+ {
243
+ if ((typeName.text === 'Object' || typeName.text === 'Record') && (typeArguments == null ? void 0 : typeArguments.length) === 2) {
244
+ return {
245
+ type: 'record',
246
+ key: toSourceTS(typeArguments[0]),
247
+ val: toSourceTS(typeArguments[1])
248
+ };
249
+ } else if (typeName.text === 'Object' && (!typeArguments || (typeArguments == null ? void 0 : typeArguments.length) === 0)) {
250
+ return {
251
+ type: 'object',
252
+ properties: {}
253
+ };
254
+ } else if (typeName.text === 'Map' && (typeArguments == null ? void 0 : typeArguments.length) === 2) {
255
+ const key = toSourceTS(typeArguments[0]);
256
+ const val = toSourceTS(typeArguments[1]);
257
+ return {
258
+ type: 'map',
259
+ key,
260
+ val
261
+ };
262
+ } else if (typeName.text === 'Array' && (typeArguments == null ? void 0 : typeArguments.length) === 1) {
263
+ const elementType = toSourceTS(typeArguments[0]);
264
+ return {
265
+ type: 'array',
266
+ elementType
267
+ };
268
+ } else if (typeName.text === 'Promise' && (typeArguments == null ? void 0 : typeArguments.length) === 1) {
269
+ const elementType = toSourceTS(typeArguments[0]);
270
+ return {
271
+ type: 'promise',
272
+ elementType
273
+ };
274
+ } else if (typeName.text === 'Set' && (typeArguments == null ? void 0 : typeArguments.length) === 1) {
275
+ const elementType = toSourceTS(typeArguments[0]);
276
+ return {
277
+ type: 'set',
278
+ elementType
279
+ };
280
+ } else if (typeName.text === 'Class' && (typeArguments == null ? void 0 : typeArguments.length) === 1) {
281
+ const elementType = toSourceTS(typeArguments[0]);
282
+ return {
283
+ type: 'class',
284
+ elementType
285
+ };
286
+ }
287
+ if (!typeArguments) {
288
+ return typeName.getText();
289
+ }
290
+ const _name = typeName.text;
291
+ const args = typeArguments.map(toSourceTS);
292
+ return {
293
+ type: 'reference',
294
+ name: _name,
295
+ args
296
+ };
297
+ }
298
+ case StringKeyword:
299
+ return node.getText();
300
+ case NumberKeyword:
301
+ return node.getText();
302
+ case NamedTupleMember:
303
+ return toSourceTS(node.type);
304
+ case IntersectionType:
305
+ {
306
+ const _members = node.types.map(toSourceTS);
307
+ return {
308
+ type: 'intersection',
309
+ members: _members
310
+ };
311
+ }
312
+ case TupleType:
313
+ const elements = node.elements.map(toSourceTS);
314
+ return {
315
+ type: 'tuple',
316
+ elements
317
+ };
318
+ case UnionType:
319
+ const members = node.types.map(toSourceTS);
320
+ return {
321
+ type: 'union',
322
+ members
323
+ };
324
+ case TypeLiteral:
325
+ const properties = {};
326
+ node.members.forEach(member => {
327
+ const name = toSourceTS(member.name);
328
+ const type = toSourceTS(member.type);
329
+ properties[name] = type;
330
+ });
331
+ return {
332
+ type: 'object',
333
+ properties
334
+ };
335
+ case PropertySignature:
336
+ console.warn('toSourceTS> should not happen, handled by TypeLiteral directly');
337
+ return `${toSourceTS(node.name)}: ${toSourceTS(node.type)}`;
338
+ case Identifier:
339
+ return node.text;
340
+ case ArrayType:
341
+ {
342
+ const elementType = toSourceTS(node.elementType);
343
+ return {
344
+ type: 'array',
345
+ elementType
346
+ };
347
+ }
348
+ case LiteralType:
349
+ return toSourceTS(node.literal);
350
+ case AnyKeyword:
351
+ case BooleanKeyword:
352
+ // ts.SyntaxKind[parseType("*").kind] === 'JSDocAllType'
353
+ case JSDocAllType:
354
+ case NullKeyword:
355
+ case StringLiteral:
356
+ case ThisType:
357
+ case UndefinedKeyword:
358
+ case VoidKeyword:
359
+ case UnknownKeyword:
360
+ case NeverKeyword:
361
+ return node.getText();
362
+ case TrueKeyword:
363
+ return true;
364
+ case FalseKeyword:
365
+ return false;
366
+ case NumericLiteral:
367
+ return Number(node.getText());
368
+ case ObjectKeyword:
369
+ return {
370
+ type: 'object',
371
+ properties: {}
372
+ };
373
+ case ParenthesizedType:
374
+ // fall-through for parentheses
375
+ return toSourceTS(node.type);
376
+ case LastTypeNode:
377
+ return toSourceTS(node.qualifier);
378
+ default:
379
+ // const test = {};
380
+ // Object.entries(ts.SyntaxKind).forEach(([name, id]) => {
381
+ // test[id] = (test[id] || []);
382
+ // test[id].push(name);
383
+ // });
384
+ // console.log(test);
385
+ console.warn('toSourceTS> unhandled kind - make sure to understand you cannot reverse TS enums');
386
+ console.warn('if they contain range aliases, for example:');
387
+ console.warn('ts.SyntaxKind.NumericLiteral === ts.SyntaxKind.FirstLiteralToken');
388
+ console.warn('so this "kind" could be wrong, but requires handling anyway:', kind_, node);
389
+ debugger;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * @todo expandTypeDepFree doesn't support "complicated" types.
395
+ * For actual builds, we use expandType() anyway (which is based on TypeScript).
396
+ * But since TypeScript is a huge dependency, I'm looking into BabelFlow/BabelTypescript parser.
397
+ * Comparing AST's like this usually helps to find bugs or potential issues,
398
+ * while we can benchmark for best performance too.
399
+ * @example
400
+ * const tooComplex = 'Array<string|{chunks?: undefined|Array<{language: string|null, timestamp: Array<number|null>, text: string}>}>';
401
+ * console.log(expandTypeDepFree(tooComplex));
402
+ */
403
+ /**
404
+ * @typedef {object} ExpandTypeReturnValue
405
+ * @property {'array' | 'union' | 'record' | 'tuple' | 'object' | 'promise' | 'typeof'} type - The type.
406
+ * @property {object | string} [elementType] - For Array.
407
+ * @property {object | string} [key] - For Record<key, val>
408
+ * @property {object | string} [val] - For Record<key, val>
409
+ * @property {(object | string)[]} [members] - For unions.
410
+ * @property {object | string} [properties] - For objects.
411
+ * @property {(object | string)[]} [elements] - For tuples.
412
+ * @property {object | string} [argument] - For typeof.
413
+ */
414
+ /**
415
+ * 'DepFree' refers to the fact that this function has no dependencies,
416
+ * while `expandType` depends on TypeScript itself for maximum compatibility.
417
+ * @example
418
+ * expandTypeDepFree('(123) '); // Outputs: '123'
419
+ * expandTypeDepFree('Array<number> '); // Outputs: { type: 'array', elementType: 'number' }
420
+ * expandTypeDepFree('Array<(123) > '); // Outputs: { type: 'array', elementType: '123' }
421
+ * expandTypeDepFree(' ( ( 123 ) ) '); // Outputs: '123'
422
+ * expandTypeDepFree(' (string ) |(number ) '); // Outputs: { type: 'union', members: ['string', 'number'] }
423
+ * expandTypeDepFree(' (( Object ) ) '); // Outputs: { type: 'object', properties: {} }
424
+ * @param {string} type - The input type to expand.
425
+ * @returns {string | ExpandTypeReturnValue} Object containing parsed information from type string.
426
+ */
427
+ function expandTypeDepFree(type) {
428
+ type = type.trim();
429
+ // '(123)' -> '123'
430
+ while (!type.includes('|') && type[0] === '(' && type[type.length - 1] === ')') {
431
+ type = type.slice(1, -1).trim();
432
+ }
433
+ // (1) Rest parameters like ...string
434
+ if (type[0] === '.' && type[1] === '.' && type[2] === '.') {
435
+ const elementType = type.slice(3);
436
+ return {
437
+ type: 'array',
438
+ elementType
439
+ };
440
+ }
441
+ // (2)
442
+ // Array<...>
443
+ if (type.startsWith("Array<") && type.endsWith('>')) {
444
+ const typeSlice = type.slice(6, -1);
445
+ const elementType = expandTypeDepFree(typeSlice);
446
+ return {
447
+ type: "array",
448
+ elementType
449
+ };
450
+ }
451
+ // Promise<...>
452
+ if (type.startsWith("Promise<") && type.endsWith('>')) {
453
+ const typeSlice = type.slice(8, -1);
454
+ const elementType = expandTypeDepFree(typeSlice);
455
+ return {
456
+ type: "promise",
457
+ elementType
458
+ };
459
+ }
460
+ // (3) Object<...> or Record<...>
461
+ if ((type.startsWith("Object<") || type.startsWith("Record<")) && type.endsWith('>')) {
462
+ const recordSlice = type.slice(7, -1);
463
+ const firstComma = recordSlice.indexOf(',');
464
+ if (firstComma === -1) {
465
+ console.warn("expandTypeDepFree> invalid Object/Record");
466
+ }
467
+ const key = recordSlice.slice(0, firstComma).trim();
468
+ const val = recordSlice.slice(firstComma + 1).trim();
469
+ return {
470
+ type: "record",
471
+ key: expandTypeDepFree(key),
472
+ val: expandTypeDepFree(val)
473
+ };
474
+ }
475
+ // (4) {...}
476
+ if (type[0] === '{' && type[type.length - 1] === '}') {
477
+ const propertiesArray = type.slice(1, -1).split(','); // ['entity: Entity', ' app: AppBase']
478
+ const properties = {};
479
+ propertiesArray.forEach(_ => {
480
+ const [propName, propType] = _.split(":").map(_ => _.trim());
481
+ if (!propName || !propType) {
482
+ console.warn('expandTypeDepFree> unexpected type format, fix');
483
+ return false;
484
+ }
485
+ properties[propName] = propType;
486
+ });
487
+ return {
488
+ type: 'object',
489
+ properties
490
+ };
491
+ }
492
+ // (5) expand unions
493
+ const members = type.split("|");
494
+ if (members.length >= 2) {
495
+ members.forEach((_, i) => members[i] = _.trim());
496
+ return {
497
+ type: 'union',
498
+ members: members.map(expandTypeDepFree)
499
+ };
500
+ }
501
+ // (6) expand [] Arrays
502
+ // Test arrays: new pc.Mat3().set([1, 2, 3, "asd"])
503
+ if (type.endsWith("[]")) {
504
+ const typeSlice = type.slice(0, -2);
505
+ return {
506
+ type: 'array',
507
+ elementType: expandTypeDepFree(typeSlice)
508
+ };
509
+ }
510
+ // (7) expand tuples
511
+ if (type[0] === '[' && type[type.length - 1] === ']') {
512
+ const elements = type.slice(1, -1).split(','); // ['null', ' Texture', ' Texture', ' Texture', ' Texture', ' Texture', ' Texture']
513
+ return {
514
+ type: 'tuple',
515
+ elements: elements.map(expandTypeDepFree)
516
+ };
517
+ }
518
+ // (8) expand typeof expressions
519
+ if (type.startsWith('typeof ')) {
520
+ const argument = expandTypeDepFree(type.substring(7));
521
+ return {
522
+ type: 'typeof',
523
+ argument
524
+ };
525
+ }
526
+ if (type === 'object' || type === 'Object') {
527
+ return {
528
+ type: 'object',
529
+ properties: {}
530
+ };
531
+ }
532
+ return type;
533
+ }
534
+
535
+ /** @typedef {import('@babel/types').Node} Node */
536
+ /** @typedef {import('@babel/types').Function} Function */
537
+ /**
538
+ * Checks if the provided node is a function-like structure.
539
+ *
540
+ * @param {Node} node - The Babel AST node to be tested.
541
+ * @returns {node is Function} - `true` if the node is a function-like structure, otherwise `false`.
542
+ */
543
+ function nodeIsFunction(node) {
544
+ switch (node.type) {
545
+ case 'ArrowFunctionExpression':
546
+ case 'ClassMethod':
547
+ case 'ClassPrivateMethod':
548
+ case 'FunctionDeclaration':
549
+ case 'FunctionExpression':
550
+ case 'ObjectMethod':
551
+ return true;
552
+ }
553
+ return false;
554
+ }
555
+
556
+ /**
557
+ * @typedef DocType
558
+ * @property {boolean} optional - Type is optional.
559
+ */
560
+ /**
561
+ * @param {string | DocType} type - The type.
562
+ * @param {boolean} optional - Optionality
563
+ * @returns {string | DocType} The simplified type.
564
+ */
565
+ function simplifyType(type, optional) {
566
+ // If it's already an object, just set optionality.
567
+ if (type instanceof Object) {
568
+ type.optional = optional;
569
+ } else if (typeof type === 'string') {
570
+ type = type.trim();
571
+ if (type !== 'object' && type !== 'object[]' && type !== 'union' && !optional) {
572
+ // console.log("simplify", type);
573
+ return type;
574
+ }
575
+ type = {
576
+ type,
577
+ optional
578
+ };
579
+ } else {
580
+ debugger;
581
+ console.warn("simplifyType> neither object nor string for type", type);
582
+ }
583
+ if (type.type === 'object' && type.properties && Object.keys(type.properties).length === 0) {
584
+ delete type.properties;
585
+ // console.log("delete empty", type);
586
+ }
587
+
588
+ return type;
589
+ }
590
+
591
+ /**
592
+ * Parses JSDoc comments to extract parameter type information.
593
+ *
594
+ * @param {string} src - The JSDoc comment string to parse.
595
+ * @param {Function} [expandType] - An optional function to process the types found in the JSDoc.
596
+ * @returns {Record<string, any> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
597
+ */
598
+ function parseJSDoc(src, expandType = expandTypeDepFree) {
599
+ // Parse something like: @param {Object} [kwargs={}] Optional arguments.
600
+ const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\{\}\.'" ]+)/g;
601
+ const matches = [...src.matchAll(regex)];
602
+ /** @type {Record<string, any>} */
603
+ const params = Object.create(null);
604
+ matches.forEach(_ => {
605
+ const type = expandType(_[1].trim());
606
+ let name = _[2].trim();
607
+ let optional = false;
608
+ // Examples:
609
+ // name: [kwargs={}] The configuration parameters.
610
+ // name: [d = 1.0] Sample spacing
222
611
  if (name[0] === '[') {
223
612
  // Possible improvement: counting opening/closing brackets for perfect match
224
613
  const closer = name.lastIndexOf(']');
@@ -2117,1284 +2506,924 @@ class Stringifier {
2117
2506
  const b = this.toSource(local);
2118
2507
  if (a !== b) {
2119
2508
  return `${a} as ${b}`;
2120
- }
2121
- return a;
2122
- }
2123
- /**
2124
- * @param {import("@babel/types").ImportDefaultSpecifier} node - The Babel AST node.
2125
- * @returns {string} Stringification of the node.
2126
- */
2127
- ImportDefaultSpecifier(node) {
2128
- const {
2129
- local
2130
- } = node;
2131
- return this.toSource(local);
2132
- }
2133
- /**
2134
- * @param {import("@babel/types").ImportNamespaceSpecifier} node - The Babel AST node.
2135
- * @returns {string} Stringification of the node.
2136
- */
2137
- ImportNamespaceSpecifier(node) {
2138
- const {
2139
- local
2140
- } = node;
2141
- return `* as ${this.toSource(local)}`;
2142
- }
2143
- /**
2144
- * @param {import("@babel/types").File} node - The Babel AST node.
2145
- * @returns {string} Stringification of the node.
2146
- */
2147
- File(node) {
2148
- return this.toSource(node.program) + '\n';
2149
- }
2150
- /**
2151
- * @param {import("@babel/types").Program} node - The Babel AST node.
2152
- * @returns {string} Stringification of the node.
2153
- */
2154
- Program(node) {
2155
- const {
2156
- /*sourceType, interpreter,*/body,
2157
- directives
2158
- } = node;
2159
- let out = '';
2160
- // @todo I would like to keep comments above and below,
2161
- // but below one is currently dropped (does't matter for AST)
2162
- // See: test/typechecking/directive.mjs
2163
- out += this.mapToSource(directives);
2164
- out += this.mapToSource(body).join('\n');
2165
- return out;
2166
- }
2167
- /**
2168
- * @param {import("@babel/types").StringLiteral} node - The Babel AST node.
2169
- * @returns {string} Stringification of the node.
2170
- */
2171
- StringLiteral(node) {
2172
- const {
2173
- extra
2174
- } = node;
2175
- // Never experienced this so far, but types are types...
2176
- if (!extra) {
2177
- debugger;
2178
- return '';
2179
- }
2180
- if (typeof extra.raw !== 'string') {
2181
- debugger;
2182
- return '';
2183
- }
2184
- return extra.raw;
2185
- }
2186
- /**
2187
- * @param {import("@babel/types").NumericLiteral} node - The Babel AST node.
2188
- * @returns {string} Stringification of the node.
2189
- */
2190
- NumericLiteral(node) {
2191
- return node.extra.raw;
2192
- }
2193
- /**
2194
- * @param {import("@babel/types").AssignmentPattern} node - The Babel AST node.
2195
- * @returns {string} Stringification of the node.
2196
- */
2197
- AssignmentPattern(node) {
2198
- const {
2199
- left,
2200
- right
2201
- } = node;
2202
- return `${this.toSource(left)} = ${this.toSource(right)}`;
2203
- }
2204
- /**
2205
- * @param {import("@babel/types").NullLiteral} node - The Babel AST node.
2206
- * @returns {string} Stringification of the node.
2207
- */
2208
- NullLiteral(node) {
2209
- return 'null';
2210
- }
2211
- /**
2212
- * @param {import("@babel/types").TaggedTemplateExpression} node - The Babel AST node.
2213
- * @returns {string} Stringification of the node.
2214
- */
2215
- TaggedTemplateExpression(node) {
2216
- const {
2217
- tag,
2218
- quasi
2219
- } = node;
2220
- const t = this.toSource(tag);
2221
- const q = this.toSource(quasi);
2222
- return t + q;
2223
- }
2224
- /**
2225
- * @param {import("@babel/types").YieldExpression} node - The Babel AST node.
2226
- * @returns {string} Stringification of the node.
2227
- */
2228
- YieldExpression(node) {
2229
- const {
2230
- delegate,
2231
- argument
2232
- } = node;
2233
- let keyword = 'yield';
2234
- if (delegate) {
2235
- keyword += '*';
2236
- }
2237
- const a = this.toSource(argument);
2238
- return `${keyword} ${a}`;
2239
- }
2240
- }
2241
-
2242
- /** @typedef {import('@babel/types').Node } Node */
2243
- /** @typedef {import("@babel/types").ClassMethod } ClassMethod */
2244
- /** @typedef {import("@babel/types").ClassPrivateMethod} ClassPrivateMethod */
2245
- /** @typedef {import('./stat.mjs').Stat } Stat */
2246
- /**
2247
- * @typedef {object} Options
2248
- * @property {boolean} [forceCurly] - Determines whether curly braces are enforced in Stringifier.
2249
- * @property {boolean} [validateDivision] - Indicates whether division operations should be validated.
2250
- * @property {Function} [expandType] - A function that expands shorthand types into full descriptions.
2251
- * @property {string} [filename] - The name of a file to which the instance pertains.
2252
- * @property {boolean} [addHeader] - Whether to add import declarations headers. Defaults to true.
2253
- * @property {string[]} [ignoreLocations] - Ignore these locations because they are known false-positives.
2254
- */
2255
- class Asserter extends Stringifier {
2256
- /**
2257
- * @param {Options} [options] - The options.
2258
- */
2259
- constructor({
2260
- forceCurly = true,
2261
- validateDivision = true,
2262
- expandType = expandTypeDepFree,
2263
- filename,
2264
- addHeader = true,
2265
- ignoreLocations = []
2266
- } = {}) {
2267
- super();
2268
- /** @type {Record<string, Stat>} */
2269
- this.stats = {
2270
- 'ArrowFunctionExpression': {
2271
- checked: 0,
2272
- unchecked: 0
2273
- },
2274
- 'ClassMethod#constructor': {
2275
- checked: 0,
2276
- unchecked: 0
2277
- },
2278
- 'ClassMethod#get': {
2279
- checked: 0,
2280
- unchecked: 0
2281
- },
2282
- 'ClassMethod#method': {
2283
- checked: 0,
2284
- unchecked: 0
2285
- },
2286
- 'ClassMethod#set': {
2287
- checked: 0,
2288
- unchecked: 0
2289
- },
2290
- 'ClassPrivateMethod#get': {
2291
- checked: 0,
2292
- unchecked: 0
2293
- },
2294
- 'ClassPrivateMethod#method': {
2295
- checked: 0,
2296
- unchecked: 0
2297
- },
2298
- 'ClassPrivateMethod#set': {
2299
- checked: 0,
2300
- unchecked: 0
2301
- },
2302
- 'FunctionDeclaration': {
2303
- checked: 0,
2304
- unchecked: 0
2305
- },
2306
- 'FunctionExpression': {
2307
- checked: 0,
2308
- unchecked: 0
2309
- },
2310
- 'ObjectMethod': {
2311
- checked: 0,
2312
- unchecked: 0
2313
- }
2314
- };
2315
- /** @type {Record<string, object>} */
2316
- this.typedefs = {};
2317
- this.forceCurly = forceCurly;
2318
- this.validateDivision = validateDivision;
2319
- // @todo collect every type + manually validate as test set
2320
- // + implement expandType using Babel Flow type parser aswell
2321
- this.expandType = expandType;
2322
- this.filename = filename;
2323
- this.addHeader = addHeader;
2324
- this.ignoreLocations = ignoreLocations;
2509
+ }
2510
+ return a;
2325
2511
  }
2326
2512
  /**
2327
- * We expand type-asserted ArrowFunctionExpressions in order to add type assertions.
2328
- * @override
2329
- * @param {import("@babel/types").ArrowFunctionExpression} node - The Babel AST node.
2513
+ * @param {import("@babel/types").ImportDefaultSpecifier} node - The Babel AST node.
2330
2514
  * @returns {string} Stringification of the node.
2331
2515
  */
2332
- ArrowFunctionExpression(node) {
2516
+ ImportDefaultSpecifier(node) {
2333
2517
  const {
2334
- async,
2335
- body,
2336
- generator,
2337
- params /*, extra*/
2518
+ local
2338
2519
  } = node;
2339
- let out = '';
2340
- if (async) {
2341
- out += 'async ';
2342
- }
2343
- if (generator) {
2344
- out += ' * ';
2345
- }
2346
- out += this.FunctionDeclarationParams(params);
2347
- out += ' =>';
2348
- if (body.type === 'BlockStatement') {
2349
- out += this.toSource(body);
2350
- } else {
2351
- out += ' {\n';
2352
- out += this.generateTypeChecks(node);
2353
- out += this.spaces + 'return ' + this.toSource(body) + ';\n';
2354
- out += '}';
2355
- }
2356
- return out;
2520
+ return this.toSource(local);
2357
2521
  }
2358
2522
  /**
2359
- * @override
2360
- * @param {import("@babel/types").ClassDeclaration} node - The Babel AST node.
2523
+ * @param {import("@babel/types").ImportNamespaceSpecifier} node - The Babel AST node.
2361
2524
  * @returns {string} Stringification of the node.
2362
2525
  */
2363
- ClassDeclaration(node) {
2526
+ ImportNamespaceSpecifier(node) {
2364
2527
  const {
2365
- id
2528
+ local
2366
2529
  } = node;
2367
- const id_ = this.toSource(id);
2368
- let out = super.ClassDeclaration(node);
2369
- out += `${this.spaces}registerClass(${id_});`;
2370
- return out;
2530
+ return `* as ${this.toSource(local)}`;
2371
2531
  }
2372
2532
  /**
2373
- * Finds the closest ancestor of the given node that matches the specified type.
2374
- *
2375
- * @param {Node} node - The starting node to search from.
2376
- * @param {T} type - Type name of the node to search for.
2377
- * @template {Node['type']} T
2378
- * @returns {Extract<Node, {type: T}>|undefined} The first ancestor node of the specified type, or undefined if none is found.
2533
+ * @param {import("@babel/types").File} node - The Babel AST node.
2534
+ * @returns {string} Stringification of the node.
2379
2535
  */
2380
- findParentOfType(node, type) {
2381
- const currentIndex = this.parents.findLastIndex(_ => _ === node);
2382
- return this.parents.findLast((_, i) => {
2383
- if (i > currentIndex) {
2384
- return false;
2385
- }
2386
- return _.type === type;
2387
- });
2536
+ File(node) {
2537
+ return this.toSource(node.program) + '\n';
2388
2538
  }
2389
2539
  /**
2390
- * Alternatively "import * as rti from ..." would also prevent "Unused external imports" warning...
2391
- * or keeping log of every single call during RTI parsing.
2392
- * @todo
2393
- * Once we went over every node, we can see if we really require registerTypef, registerClass etc.
2394
- * @returns {string} The import declaration header for importing RTI.
2540
+ * @param {import("@babel/types").Program} node - The Babel AST node.
2541
+ * @returns {string} Stringification of the node.
2395
2542
  */
2396
- getHeader() {
2397
- if (!this.addHeader) {
2398
- return '';
2399
- }
2400
- let header = "import {inspectType, youCanAddABreakpointHere";
2401
- if (this.validateDivision) {
2402
- header += ", validateDivision";
2403
- }
2404
- header += ", registerTypedef, registerClass} from '@runtime-type-inspector/runtime';\n";
2405
- // Prevent tree-shaking in UMD build so we can always "add a breakpoint here".
2406
- header += "export * from '@runtime-type-inspector/runtime';\n";
2407
- return header;
2543
+ Program(node) {
2544
+ const {
2545
+ /*sourceType, interpreter,*/body,
2546
+ directives
2547
+ } = node;
2548
+ let out = '';
2549
+ // @todo I would like to keep comments above and below,
2550
+ // but below one is currently dropped (does't matter for AST)
2551
+ // See: test/typechecking/directive.mjs
2552
+ out += this.mapToSource(directives);
2553
+ out += this.mapToSource(body).join('\n');
2554
+ return out;
2408
2555
  }
2409
2556
  /**
2410
- * Retrieves the node associated with the leading comments for an arrow function expression.
2411
- *
2412
- * This method travels up the syntax tree from the given node to find a parent node with leading comments.
2413
- * This search is bounded by function boundaries or a 'CallExpression' node, as per the logic defined within the loop.
2414
- * @param {Node} node - The node representing the arrow function expression for which to find the leading comments node.
2415
- * @returns {Node|undefined} The node that contains the leading comments, or `undefined`
2416
- * if none is found before reaching a different function or 'CallExpression'.
2557
+ * @param {import("@babel/types").StringLiteral} node - The Babel AST node.
2558
+ * @returns {string} Stringification of the node.
2417
2559
  */
2418
- getLeadingCommentsNodeForArrowFunctionExpression(node) {
2560
+ StringLiteral(node) {
2419
2561
  const {
2420
- parents
2421
- } = this;
2422
- let i = parents.findLastIndex(_ => _ === node);
2423
- let parent = parents[i];
2424
- if (parent.leadingComments) {
2425
- return parent;
2562
+ extra
2563
+ } = node;
2564
+ // Never experienced this so far, but types are types...
2565
+ if (!extra) {
2566
+ debugger;
2567
+ return '';
2426
2568
  }
2427
- // Skip now, if we find another function first,
2428
- // there is no JSDoc for our function anymore.
2429
- // Not interested in our start node if it didn't
2430
- // contain leadingComments.
2431
- i--;
2432
- while (i >= 0) {
2433
- parent = parents[i];
2434
- //if (parent.type === 'CallExpression') {
2435
- // break;
2436
- //}
2437
- if (nodeIsFunction(parent)) {
2438
- break;
2439
- }
2440
- if (parent.leadingComments) {
2441
- return parent;
2442
- }
2443
- i--;
2569
+ if (typeof extra.raw !== 'string') {
2570
+ debugger;
2571
+ return '';
2444
2572
  }
2573
+ return extra.raw;
2445
2574
  }
2446
2575
  /**
2447
- * Emits a warning message to the console, optionally prefixed with the instance's filename.
2448
- * @param {...any} args - A list of arguments to be passed to the console.warn function.
2576
+ * @param {import("@babel/types").NumericLiteral} node - The Babel AST node.
2577
+ * @returns {string} Stringification of the node.
2449
2578
  */
2450
- warn(...args) {
2451
- if (this.filename) {
2452
- console.warn("[WARN]", this.filename);
2453
- }
2454
- console.warn(...args);
2579
+ NumericLiteral(node) {
2580
+ return node.extra.raw;
2455
2581
  }
2456
- getLeadingCommentsNodeForFunctionExpression(node) {
2582
+ /**
2583
+ * @param {import("@babel/types").AssignmentPattern} node - The Babel AST node.
2584
+ * @returns {string} Stringification of the node.
2585
+ */
2586
+ AssignmentPattern(node) {
2457
2587
  const {
2458
- parents
2459
- } = this;
2460
- let i = parents.findLastIndex(_ => _ === node);
2461
- let parent = parents[i];
2462
- if (parent.leadingComments) {
2463
- return parent;
2464
- }
2465
- // Skip now, if we find another function first,
2466
- // there is no JSDoc for our function anymore.
2467
- // Not interested in our start node if it didn't
2468
- // contain leadingComments.
2469
- i--;
2470
- while (i >= 0) {
2471
- parent = parents[i];
2472
- //if (parent.type === 'CallExpression') {
2473
- // break;
2474
- //}
2475
- if (nodeIsFunction(parent)) {
2476
- break;
2477
- }
2478
- if (parent.leadingComments) {
2479
- return parent;
2480
- }
2481
- i--;
2482
- }
2483
- /** @todo convert all files in test/typechecking/*.mjs into full unit tests */
2484
- // Old way:
2485
- // if (node.leadingComments) {
2486
- // return node.leadingComments;
2487
- // }
2488
- // node = this.findParentOfType(node, 'ExpressionStatement');
2489
- // if (!node) {
2490
- // /**
2491
- // * @todo Need more refactoring, see missing type-assertions in test/typechecking/good-old-es5.mjs
2492
- // */
2493
- // node = this.parents.findLast(_ => _.type === 'VariableDeclaration');
2494
- // if (!node) {
2495
- // return;
2496
- // }
2497
- // }
2588
+ left,
2589
+ right
2590
+ } = node;
2591
+ return `${this.toSource(left)} = ${this.toSource(right)}`;
2498
2592
  }
2499
2593
  /**
2500
- * @param {Node} node - The Babel AST node.
2501
- * @returns {undefined | {}} The return value of `parseJSDoc`.
2594
+ * @param {import("@babel/types").NullLiteral} node - The Babel AST node.
2595
+ * @returns {string} Stringification of the node.
2502
2596
  */
2503
- getJSDoc(node) {
2504
- if (node.type === 'BlockStatement') {
2505
- node = this.parent;
2506
- }
2507
- let {
2508
- leadingComments
2509
- } = node;
2510
- // Receive the leadingComments from the ExpressionStatement, not the FunctionExpression itself.
2511
- if (node.type === 'FunctionExpression') {
2512
- const tmp = this.getLeadingCommentsNodeForFunctionExpression(node);
2513
- leadingComments = tmp == null ? void 0 : tmp.leadingComments;
2514
- }
2515
- // Receive the leadingComments from ExportNamedDeclaration, if FunctionDeclaration has none
2516
- if (!leadingComments) {
2517
- if (node.type === 'FunctionDeclaration') {
2518
- const exportNamedDeclaration = this.findParentOfType(node, 'ExportNamedDeclaration');
2519
- leadingComments = exportNamedDeclaration == null ? void 0 : exportNamedDeclaration.leadingComments;
2520
- }
2521
- if (node.type === 'ArrowFunctionExpression') {
2522
- const tmp = this.getLeadingCommentsNodeForArrowFunctionExpression(node);
2523
- leadingComments = tmp == null ? void 0 : tmp.leadingComments;
2524
- }
2525
- }
2526
- if (leadingComments && leadingComments.length) {
2527
- const lastComment = leadingComments[leadingComments.length - 1];
2528
- if (lastComment.type === "CommentBlock") {
2529
- if (lastComment.value.includes('@event')) {
2530
- return;
2531
- }
2532
- if (node.type === 'ClassMethod' && node.kind === 'set') {
2533
- const paramName = this.getNameOfParam(node.params[0]);
2534
- if (node.params.length !== 1) {
2535
- this.warn("getJSDoc> setters require exactly one argument");
2536
- }
2537
- return {
2538
- [paramName]: parseJSDocSetter(lastComment.value, this.expandType)
2539
- };
2540
- }
2541
- if (lastComment.value.includes('@ignoreRTI')) {
2542
- return;
2543
- }
2544
- return parseJSDoc(lastComment.value, this.expandType);
2545
- }
2546
- }
2597
+ NullLiteral(node) {
2598
+ return 'null';
2599
+ }
2600
+ /**
2601
+ * @param {import("@babel/types").TaggedTemplateExpression} node - The Babel AST node.
2602
+ * @returns {string} Stringification of the node.
2603
+ */
2604
+ TaggedTemplateExpression(node) {
2605
+ const {
2606
+ tag,
2607
+ quasi
2608
+ } = node;
2609
+ const t = this.toSource(tag);
2610
+ const q = this.toSource(quasi);
2611
+ return t + q;
2547
2612
  }
2548
2613
  /**
2549
- * Retrieves the name of a parameter from a Babel AST node.
2550
- *
2551
- * This function expects a node representing a function parameter and attempts to extract
2552
- * the parameter's name directly or from an AssignmentPattern.
2553
- *
2554
- * @param {Node} param - The AST node representing the function parameter from which to extract the name.
2555
- * @returns {string} The name of the parameter as a string, or the parameter's source code if the extraction fails.
2614
+ * @param {import("@babel/types").YieldExpression} node - The Babel AST node.
2615
+ * @returns {string} Stringification of the node.
2556
2616
  */
2557
- getNameOfParam(param) {
2558
- if (param.type === 'Identifier') {
2559
- return param.name;
2560
- } else if (param.type === 'AssignmentPattern') {
2561
- if (param.left.type === 'Identifier') {
2562
- return param.left.name;
2563
- }
2617
+ YieldExpression(node) {
2618
+ const {
2619
+ delegate,
2620
+ argument
2621
+ } = node;
2622
+ let keyword = 'yield';
2623
+ if (delegate) {
2624
+ keyword += '*';
2564
2625
  }
2565
- debugger;
2566
- this.warn("unable to extra name from param in specified way - may contain too much information");
2567
- return this.toSource(param);
2568
- }
2569
- statsReset() {
2570
- Object.values(this.stats).forEach(statReset);
2626
+ const a = this.toSource(argument);
2627
+ return `${keyword} ${a}`;
2571
2628
  }
2572
- statsPrint() {
2573
- console.table(this.stats);
2629
+ }
2630
+
2631
+ /** @typedef {import('@babel/types').Node } Node */
2632
+ /** @typedef {import("@babel/types").ClassMethod } ClassMethod */
2633
+ /** @typedef {import("@babel/types").ClassPrivateMethod} ClassPrivateMethod */
2634
+ /** @typedef {import('./stat.mjs').Stat } Stat */
2635
+ /**
2636
+ * @typedef {object} Options
2637
+ * @property {boolean} [forceCurly] - Determines whether curly braces are enforced in Stringifier.
2638
+ * @property {boolean} [validateDivision] - Indicates whether division operations should be validated.
2639
+ * @property {Function} [expandType] - A function that expands shorthand types into full descriptions.
2640
+ * @property {string} [filename] - The name of a file to which the instance pertains.
2641
+ * @property {boolean} [addHeader] - Whether to add import declarations headers. Defaults to true.
2642
+ * @property {string[]} [ignoreLocations] - Ignore these locations because they are known false-positives.
2643
+ */
2644
+ class Asserter extends Stringifier {
2645
+ /**
2646
+ * @param {Options} [options] - The options.
2647
+ */
2648
+ constructor({
2649
+ forceCurly = true,
2650
+ validateDivision = true,
2651
+ expandType = expandTypeDepFree,
2652
+ filename,
2653
+ addHeader = true,
2654
+ ignoreLocations = []
2655
+ } = {}) {
2656
+ super();
2657
+ /** @type {Record<string, Stat>} */
2658
+ this.stats = {
2659
+ 'ArrowFunctionExpression': {
2660
+ checked: 0,
2661
+ unchecked: 0
2662
+ },
2663
+ 'ClassMethod#constructor': {
2664
+ checked: 0,
2665
+ unchecked: 0
2666
+ },
2667
+ 'ClassMethod#get': {
2668
+ checked: 0,
2669
+ unchecked: 0
2670
+ },
2671
+ 'ClassMethod#method': {
2672
+ checked: 0,
2673
+ unchecked: 0
2674
+ },
2675
+ 'ClassMethod#set': {
2676
+ checked: 0,
2677
+ unchecked: 0
2678
+ },
2679
+ 'ClassPrivateMethod#get': {
2680
+ checked: 0,
2681
+ unchecked: 0
2682
+ },
2683
+ 'ClassPrivateMethod#method': {
2684
+ checked: 0,
2685
+ unchecked: 0
2686
+ },
2687
+ 'ClassPrivateMethod#set': {
2688
+ checked: 0,
2689
+ unchecked: 0
2690
+ },
2691
+ 'FunctionDeclaration': {
2692
+ checked: 0,
2693
+ unchecked: 0
2694
+ },
2695
+ 'FunctionExpression': {
2696
+ checked: 0,
2697
+ unchecked: 0
2698
+ },
2699
+ 'ObjectMethod': {
2700
+ checked: 0,
2701
+ unchecked: 0
2702
+ }
2703
+ };
2704
+ /** @type {Record<string, object>} */
2705
+ this.typedefs = {};
2706
+ this.forceCurly = forceCurly;
2707
+ this.validateDivision = validateDivision;
2708
+ // @todo collect every type + manually validate as test set
2709
+ // + implement expandType using Babel Flow type parser aswell
2710
+ this.expandType = expandType;
2711
+ this.filename = filename;
2712
+ this.addHeader = addHeader;
2713
+ this.ignoreLocations = ignoreLocations;
2574
2714
  }
2575
2715
  /**
2576
- * Retrieves statistical information for a given Babel AST node of this instance.
2577
- *
2578
- * @param {Node} node - The Babel AST node for which the statistical data is retrieved.
2579
- * @returns {Stat} An object containing the statistical data for the specified node. If the
2580
- * node type is unhandled, defaults to returning a dummy object with 'checked' and 'unchecked'
2581
- * properties both set to 0.
2716
+ * We expand type-asserted ArrowFunctionExpressions in order to add type assertions.
2717
+ * @override
2718
+ * @param {import("@babel/types").ArrowFunctionExpression} node - The Babel AST node.
2719
+ * @returns {string} Stringification of the node.
2582
2720
  */
2583
- getStatsForNode(node) {
2721
+ ArrowFunctionExpression(node) {
2584
2722
  const {
2585
- stats
2586
- } = this;
2587
- const type = nodeIsFunction(node) ? node.type : this.parentType;
2588
- if (type === 'ClassMethod') {
2589
- const parent = /** @type {ClassMethod} */
2590
- this.parent;
2591
- const {
2592
- kind
2593
- } = parent;
2594
- return stats[`ClassMethod#${kind}`];
2595
- } else if (type === 'ClassPrivateMethod') {
2596
- const parent = /** @type {ClassPrivateMethod} */
2597
- this.parent;
2598
- const {
2599
- kind
2600
- } = parent;
2601
- return stats[`ClassPrivateMethod#${kind}`];
2723
+ async,
2724
+ body,
2725
+ generator,
2726
+ params /*, extra*/
2727
+ } = node;
2728
+ let out = '';
2729
+ if (async) {
2730
+ out += 'async ';
2602
2731
  }
2603
- const stat = stats[type];
2604
- if (!stat) {
2605
- this.warn("getStatsForNode> dummy, but unhandled... fix for node type", node);
2606
- return {
2607
- checked: 0,
2608
- unchecked: 0
2609
- };
2732
+ if (generator) {
2733
+ out += ' * ';
2610
2734
  }
2611
- return stat;
2735
+ out += this.FunctionDeclarationParams(params);
2736
+ out += ' =>';
2737
+ if (body.type === 'BlockStatement') {
2738
+ out += this.toSource(body);
2739
+ } else {
2740
+ out += ' {\n';
2741
+ out += this.generateTypeChecks(node);
2742
+ out += this.spaces + 'return ' + this.toSource(body) + ';\n';
2743
+ out += '}';
2744
+ }
2745
+ return out;
2612
2746
  }
2613
2747
  /**
2614
- * Checks if a provided Babel AST node has a parameter with the given name.
2615
- *
2616
- * This function will look at the node's parameters if available and determine whether
2617
- * one of them matches the provided name. Supports various parameter types such as Identifiers
2618
- * and AssignmentPatterns.
2619
- *
2620
- * @param {Node} node - The Babel AST node to inspect. If it's a BlockStatement, the parent node is used instead.
2621
- * @param {string} name - The name of the parameter to look for within the node's parameters.
2622
- * @returns {boolean} True if the node has a parameter with the given name; false otherwise.
2748
+ * @override
2749
+ * @param {import("@babel/types").ClassDeclaration} node - The Babel AST node.
2750
+ * @returns {string} Stringification of the node.
2623
2751
  */
2624
- nodeHasParamName(node, name) {
2625
- if (node.type === 'BlockStatement') {
2626
- node = this.parent;
2627
- }
2752
+ ClassDeclaration(node) {
2628
2753
  const {
2629
- params
2754
+ id
2630
2755
  } = node;
2631
- if (!params) {
2632
- this.warn("nodeHasParamName> Expected params for", {
2633
- node,
2634
- name
2635
- });
2636
- return false;
2637
- }
2638
- return params.some(node => {
2639
- const {
2640
- type
2641
- } = node;
2642
- if (type === "AssignmentPattern") {
2643
- const {
2644
- left
2645
- } = node;
2646
- console.assert(left.type === 'Identifier' || left.type === 'ObjectPattern' || left.type === 'ArrayPattern' || left.type === 'AssignmentPattern', 'Expected Identifier or ObjectPattern');
2647
- return left.name === name;
2648
- } else if (type === 'Identifier') {
2649
- return node.name === name;
2650
- } else if (type === 'ArrayPattern' || type === 'ObjectPattern' || type === 'RestElement') {
2756
+ const id_ = this.toSource(id);
2757
+ let out = super.ClassDeclaration(node);
2758
+ out += `${this.spaces}registerClass(${id_});`;
2759
+ return out;
2760
+ }
2761
+ /**
2762
+ * Finds the closest ancestor of the given node that matches the specified type.
2763
+ *
2764
+ * @param {Node} node - The starting node to search from.
2765
+ * @param {T} type - Type name of the node to search for.
2766
+ * @template {Node['type']} T
2767
+ * @returns {Extract<Node, {type: T}>|undefined} The first ancestor node of the specified type, or undefined if none is found.
2768
+ */
2769
+ findParentOfType(node, type) {
2770
+ const currentIndex = this.parents.findLastIndex(_ => _ === node);
2771
+ return this.parents.findLast((_, i) => {
2772
+ if (i > currentIndex) {
2651
2773
  return false;
2652
2774
  }
2653
- const _ = new Stringifier();
2654
- const code = _.toSource(node);
2655
- console.log("Unknown type to test params for", type, code);
2656
- return false;
2775
+ return _.type === type;
2657
2776
  });
2658
2777
  }
2659
2778
  /**
2660
- * Generates a string containing type checks for a given Babel AST node based on associated JSDoc information.
2661
- *
2662
- * This function analyzes the node and its JSDoc annotations to construct runtime type
2663
- * check expressions. It handles various parameter patterns and outputs code that performs
2664
- * actual type assertions. If a node does not correspond to any known or supported pattern,
2665
- * it returns an empty string.
2666
- *
2667
- * @override
2668
- * @param {Node} node - The Babel AST node for which to generate type checks.
2669
- * @returns {string} A string of code with type check assertions, based on the JSDoc comments associated with the given node.
2779
+ * Alternatively "import * as rti from ..." would also prevent "Unused external imports" warning...
2780
+ * or keeping log of every single call during RTI parsing.
2781
+ * @todo
2782
+ * Once we went over every node, we can see if we really require registerTypef, registerClass etc.
2783
+ * @returns {string} The import declaration header for importing RTI.
2670
2784
  */
2671
- generateTypeChecks(node) {
2672
- const {
2673
- parent
2674
- } = this;
2675
- if (node.type === 'BlockStatement' && !nodeIsFunction(parent)) {
2785
+ getHeader() {
2786
+ if (!this.addHeader) {
2676
2787
  return '';
2677
2788
  }
2678
- const jsdoc = this.getJSDoc(node);
2679
- // return '// ' + JSON.stringify(jsdoc) + '\n';
2680
- const stat = this.getStatsForNode(node);
2681
- if (!jsdoc) {
2682
- stat.unchecked++;
2683
- return '';
2789
+ let header = "import {inspectType, youCanAddABreakpointHere, registerVariable";
2790
+ if (this.validateDivision) {
2791
+ header += ", validateDivision";
2684
2792
  }
2685
- stat.checked++;
2793
+ header += ", registerTypedef, registerClass} from '@runtime-type-inspector/runtime';\n";
2794
+ // Prevent tree-shaking in UMD build so we can always "add a breakpoint here".
2795
+ header += "export * from '@runtime-type-inspector/runtime';\n";
2796
+ return header;
2797
+ }
2798
+ /**
2799
+ * Retrieves the node associated with the leading comments for an arrow function expression.
2800
+ *
2801
+ * This method travels up the syntax tree from the given node to find a parent node with leading comments.
2802
+ * This search is bounded by function boundaries or a 'CallExpression' node, as per the logic defined within the loop.
2803
+ * @param {Node} node - The node representing the arrow function expression for which to find the leading comments node.
2804
+ * @returns {Node|undefined} The node that contains the leading comments, or `undefined`
2805
+ * if none is found before reaching a different function or 'CallExpression'.
2806
+ */
2807
+ getLeadingCommentsNodeForArrowFunctionExpression(node) {
2686
2808
  const {
2687
- spaces
2809
+ parents
2688
2810
  } = this;
2689
- let out = '';
2690
- let first = true;
2691
- const loc = this.getName(node);
2692
- if (this.ignoreLocations.includes(loc)) {
2693
- return '// IGNORE RTI TYPE VALIDATIONS, KNOWN ISSUES\n';
2811
+ let i = parents.findLastIndex(_ => _ === node);
2812
+ let parent = parents[i];
2813
+ if (parent.leadingComments) {
2814
+ return parent;
2694
2815
  }
2695
- //out += `${spaces}/*${spaces} node.type=${node.type}\n${spaces}
2696
- // ${JSON.stringify(jsdoc)}\n${parent}\n${spaces}*/\n`;
2697
- for (let name in jsdoc) {
2698
- const type = jsdoc[name];
2699
- const hasParam = this.nodeHasParamName(node, name);
2700
- if (!hasParam) {
2701
- let testNode = node;
2702
- if (node.type === 'BlockStatement') {
2703
- testNode = this.parent;
2704
- }
2705
- const paramIndex = Object.keys(jsdoc).findIndex(_ => _ === name);
2706
- const param = testNode.params[paramIndex];
2707
- if (param) {
2708
- const isObjectPattern = param.type === 'ObjectPattern';
2709
- const isArrayPattern = param.type === 'ArrayPattern';
2710
- const isSupportedPattern = isObjectPattern || isArrayPattern;
2711
- // There are four kinds of patterns:
2712
- // ObjectPattern:
2713
- // function test({x = 123}) {return x;} test({x: 456});
2714
- // ArrayPattern:
2715
- // function test([x = 123]) {return x;}; test([456]);
2716
- // AssignmentPattern made up of ObjectPattern:
2717
- // function test({x = 123} = {}) {return x;} test();
2718
- // AssignmentPattern made up of ArrayPattern:
2719
- // function test([x = 123] = []) {return x;} test();
2720
- if (isSupportedPattern) {
2721
- // The name doesn't matter any longer, because any pattern inherently
2722
- // drops the identifier from the AST. But we can access it
2723
- // via arguments[paramIndex] anyway.
2724
- name = `arguments[${paramIndex}]`;
2725
- } else if (param.type === 'AssignmentPattern') {
2726
- const _loc = this.getName(node);
2727
- if (param.left.type === 'ArrayPattern' && type.type === 'array') {
2728
- // Add a type assertion for each element of the ArrayPattern
2729
- for (const element of param.left.elements) {
2730
- if (element.type !== 'Identifier') {
2731
- this.warn('Only Identifier case handled right now');
2732
- continue;
2733
- }
2734
- const _t = JSON.stringify(type.elementType, null, 2).replaceAll('\n', '\n' + spaces);
2735
- out += `${spaces}if (!inspectType(${element.name}, ${_t}, '${_loc}', '${name}')) {\n`;
2736
- out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
2737
- }
2738
- continue;
2739
- } else if (param.left.type === 'ObjectPattern' && type.type === 'object') {
2740
- // Add a type assertion for each property of the ObjectPattern
2741
- for (const property of param.left.properties) {
2742
- if (property.key.type !== 'Identifier') {
2743
- this.warn('ObjectPattern> Only Identifier case handled right now');
2744
- continue;
2745
- }
2746
- const keyName = property.key.name;
2747
- if (type.type !== 'object' || !type.properties) {
2748
- this.warn("missing subtype information in JSDoc> in type", JSON.stringify(type, null, 2), "for ObjectPattern:", this.toSource(property).trim());
2749
- continue;
2750
- }
2751
- const subType = type.properties[keyName];
2752
- if (!subType) {
2753
- this.warn("missing subtype information in JSDoc");
2754
- continue;
2755
- }
2756
- const _t2 = JSON.stringify(subType, null, 2).replaceAll('\n', '\n' + spaces);
2757
- out += `${spaces}if (!inspectType(${keyName}, ${_t2}, '${_loc}', '${name}')) {\n`;
2758
- out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
2759
- }
2760
- continue;
2761
- }
2762
- this.warn(`generateTypeChecks> ${_loc}> todo implement`, `AssignmentPattern for parameter ${name}`);
2763
- continue;
2764
- }
2765
- } else {
2766
- const _loc2 = this.getName(node);
2767
- this.warn(`generateTypeChecks> ${_loc2}> Missing param: ${name}`);
2768
- continue;
2769
- }
2770
- }
2771
- let t = JSON.stringify(type, null, 2).replaceAll('\n', '\n' + spaces);
2772
- if (type === 'this') {
2773
- const classDecl = this.findParentOfType(node, 'ClassDeclaration');
2774
- if (!(classDecl != null && classDecl.id)) {
2775
- this.warn('generateTypeChecks> !classDecl?.id');
2776
- }
2777
- t = '"' + this.toSource(classDecl.id) + '"';
2778
- }
2779
- const _loc3 = this.getName(node);
2780
- let prevCheck = '';
2781
- // JSDoc doesn't support multiple function signatures yet, but this is
2782
- // exactly what we would need to deal with ObjectPool'ing
2783
- if (_loc3 === 'ContactPoint#constructor' || _loc3 === 'ContactResult#constructor' || _loc3 === 'SingleContactResult#constructor') {
2784
- prevCheck = 'arguments.length !== 0 && ';
2816
+ // Skip now, if we find another function first,
2817
+ // there is no JSDoc for our function anymore.
2818
+ // Not interested in our start node if it didn't
2819
+ // contain leadingComments.
2820
+ i--;
2821
+ while (i >= 0) {
2822
+ parent = parents[i];
2823
+ //if (parent.type === 'CallExpression') {
2824
+ // break;
2825
+ //}
2826
+ if (nodeIsFunction(parent)) {
2827
+ break;
2785
2828
  }
2786
- if (first) {
2787
- out += '\n';
2788
- first = false;
2829
+ if (parent.leadingComments) {
2830
+ return parent;
2789
2831
  }
2790
- out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${_loc3}', '${name}')) {\n`;
2791
- out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
2832
+ i--;
2792
2833
  }
2793
- return out;
2794
2834
  }
2795
2835
  /**
2796
- * @param {Node} node - The Babel AST node.
2797
- * @returns {string} Best possible human-readable name of given node.
2836
+ * Emits a warning message to the console, optionally prefixed with the instance's filename.
2837
+ * @param {...any} args - A list of arguments to be passed to the console.warn function.
2798
2838
  */
2799
- getNameForFunctionExpression(node) {
2800
- const objectProperty = this.findParentOfType(node, 'ObjectProperty');
2801
- if (objectProperty) {
2802
- // See good-old-es5.mjs example for a test case
2803
- // TODO: Make an even better name based on Object.assign(ScopeSpace.prototype
2804
- // Ideally we would figure out the name: ScopeSpace#resolve
2805
- // Currently we only find "resolve" (still better than 'unnamed function expression'...)
2806
- return this.toSource(objectProperty.key);
2807
- }
2808
- const expressionStatement = this.findParentOfType(node, 'ExpressionStatement');
2809
- if (expressionStatement) {
2810
- // There are many kinds of expressions
2811
- // type Expression = ArrayExpression | AssignmentExpression | BinaryExpression | CallExpression | ...
2812
- const {
2813
- left
2814
- } = expressionStatement.expression;
2815
- if (left) {
2816
- return this.toSource(left);
2817
- }
2818
- console.warn("Asserter#getNameForFunctionExpression> expression without left");
2839
+ warn(...args) {
2840
+ if (this.filename) {
2841
+ console.warn("[WARN]", this.filename);
2819
2842
  }
2820
- return 'unnamed function expression';
2843
+ console.warn(...args);
2821
2844
  }
2822
- /**
2823
- * @param {Node} node - The Babel AST node.
2824
- * @returns {string} Stringification of the node.
2825
- */
2826
- getName(node) {
2827
- const toSource = this.toSource.bind(this);
2828
- if (node.type === 'BlockStatement') {
2829
- node = this.parent;
2830
- }
2845
+ getLeadingCommentsNodeForFunctionExpression(node) {
2831
2846
  const {
2832
- type,
2833
- key,
2834
- id
2835
- } = node;
2836
- switch (type) {
2837
- case 'FunctionDeclaration':
2838
- return toSource(id);
2839
- case 'ClassMethod':
2840
- case 'ClassPrivateMethod':
2841
- const classDecl = this.parents.findLast(_ => _.type === 'ClassDeclaration');
2842
- let out = '';
2843
- if (classDecl) {
2844
- out += toSource(classDecl.id) + '#';
2845
- }
2846
- out += toSource(key);
2847
- return out;
2848
- case 'FunctionExpression':
2849
- if (id) {
2850
- return toSource(id);
2851
- }
2852
- return this.getNameForFunctionExpression(node);
2853
- case 'ArrowFunctionExpression':
2854
- const parent = this.findParentOfType(node, 'VariableDeclarator');
2855
- if (parent) {
2856
- return toSource(parent.id);
2857
- }
2858
- return 'getName> missing parent for ' + node.type;
2859
- case 'ObjectMethod':
2860
- return toSource(key);
2861
- default:
2862
- this.warn('getName> unhandled type', type, 'for', node);
2863
- return '/*MISSING*/';
2847
+ parents
2848
+ } = this;
2849
+ let i = parents.findLastIndex(_ => _ === node);
2850
+ let parent = parents[i];
2851
+ if (parent.leadingComments) {
2852
+ return parent;
2853
+ }
2854
+ // Skip now, if we find another function first,
2855
+ // there is no JSDoc for our function anymore.
2856
+ // Not interested in our start node if it didn't
2857
+ // contain leadingComments.
2858
+ i--;
2859
+ while (i >= 0) {
2860
+ parent = parents[i];
2861
+ //if (parent.type === 'CallExpression') {
2862
+ // break;
2863
+ //}
2864
+ if (nodeIsFunction(parent)) {
2865
+ break;
2866
+ }
2867
+ if (parent.leadingComments) {
2868
+ return parent;
2869
+ }
2870
+ i--;
2864
2871
  }
2872
+ /** @todo convert all files in test/typechecking/*.mjs into full unit tests */
2873
+ // Old way:
2874
+ // if (node.leadingComments) {
2875
+ // return node.leadingComments;
2876
+ // }
2877
+ // node = this.findParentOfType(node, 'ExpressionStatement');
2878
+ // if (!node) {
2879
+ // /**
2880
+ // * @todo Need more refactoring, see missing type-assertions in test/typechecking/good-old-es5.mjs
2881
+ // */
2882
+ // node = this.parents.findLast(_ => _.type === 'VariableDeclaration');
2883
+ // if (!node) {
2884
+ // return;
2885
+ // }
2886
+ // }
2865
2887
  }
2866
2888
  /**
2867
- * @override
2868
- * @param {import("@babel/types").BinaryExpression} node - The Babel AST node.
2869
- * @returns {string} Stringification of the node.
2889
+ * @param {Node} node - The Babel AST node.
2890
+ * @returns {undefined | {}} The return value of `parseJSDoc`.
2870
2891
  */
2871
- BinaryExpression(node) {
2872
- if (!this.validateDivision) {
2873
- return super.BinaryExpression(node);
2892
+ getJSDoc(node) {
2893
+ if (node.type === 'BlockStatement') {
2894
+ node = this.parent;
2874
2895
  }
2875
- const {
2876
- left,
2877
- operator,
2878
- right
2896
+ let {
2897
+ leadingComments
2879
2898
  } = node;
2880
- const left_ = this.toSource(left);
2881
- const right_ = this.toSource(right);
2882
- if (operator === '/') {
2883
- return `validateDivision(${left_}, ${right_})`;
2899
+ // Receive the leadingComments from the ExpressionStatement, not the FunctionExpression itself.
2900
+ if (node.type === 'FunctionExpression') {
2901
+ const tmp = this.getLeadingCommentsNodeForFunctionExpression(node);
2902
+ leadingComments = tmp == null ? void 0 : tmp.leadingComments;
2884
2903
  }
2885
- return `${left_} ${operator} ${right_}`;
2886
- }
2887
- /**
2888
- * @override
2889
- * @param {import("@babel/types").File} node - The Babel AST node.
2890
- * @returns {string} Stringification of the node.
2891
- */
2892
- File(node) {
2893
- // @todo figure out why errors is in Babel node and not in @babel/types...
2894
- const {
2895
- /*errors,*/program,
2896
- comments
2897
- } = node;
2898
- if (comments) {
2899
- for (const comment of comments) {
2900
- const warn = this.warn.bind(this);
2901
- parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
2904
+ // Receive the leadingComments from ExportNamedDeclaration, if FunctionDeclaration has none
2905
+ if (!leadingComments) {
2906
+ if (node.type === 'FunctionDeclaration') {
2907
+ const exportNamedDeclaration = this.findParentOfType(node, 'ExportNamedDeclaration');
2908
+ leadingComments = exportNamedDeclaration == null ? void 0 : exportNamedDeclaration.leadingComments;
2909
+ }
2910
+ if (node.type === 'ArrowFunctionExpression') {
2911
+ const tmp = this.getLeadingCommentsNodeForArrowFunctionExpression(node);
2912
+ leadingComments = tmp == null ? void 0 : tmp.leadingComments;
2902
2913
  }
2903
2914
  }
2904
- //console.log("this.typedefs", this.typedefs);
2905
- let out = '';
2906
- for (const name in this.typedefs) {
2907
- const typedef = this.typedefs[name];
2908
- const json = JSON.stringify(typedef, null, 2);
2909
- out += `registerTypedef('${name}', ${json});\n`;
2915
+ if (leadingComments && leadingComments.length) {
2916
+ const lastComment = leadingComments[leadingComments.length - 1];
2917
+ if (lastComment.type === "CommentBlock") {
2918
+ if (lastComment.value.includes('@event')) {
2919
+ return;
2920
+ }
2921
+ if (node.type === 'ClassMethod' && node.kind === 'set') {
2922
+ const paramName = this.getNameOfParam(node.params[0]);
2923
+ if (node.params.length !== 1) {
2924
+ this.warn("getJSDoc> setters require exactly one argument");
2925
+ }
2926
+ return {
2927
+ [paramName]: parseJSDocSetter(lastComment.value, this.expandType)
2928
+ };
2929
+ }
2930
+ if (lastComment.value.includes('@ignoreRTI')) {
2931
+ return;
2932
+ }
2933
+ return parseJSDoc(lastComment.value, this.expandType);
2934
+ }
2910
2935
  }
2911
- const code = this.toSource(program) + '\n';
2912
- out += code;
2913
- return out;
2914
- }
2915
- }
2916
-
2917
- /**
2918
- * Simple facade which does all the processing. Processes the input
2919
- * source string, adding runtime type checks based on JSDoc comments.
2920
- *
2921
- * This function takes JavaScript source code as input, parses it to an AST, traverses the
2922
- * AST to find type annotations in JSDoc comments, and generates appropriate runtime type
2923
- * assertions. These are then inserted into the source, producing a new version of the code
2924
- * that includes runtime type checking based on the original JSDoc annotations.
2925
- *
2926
- * @param {string} src - The input source code containing JSDoc comments to be processed
2927
- * for type checks.
2928
- * @param {import('./Asserter.mjs').Options} [options] - Configuration options that dictate
2929
- * how the processing is performed.
2930
- * @returns {string} The transformed source code with inserted runtime type checks, or the
2931
- * original source code commented with an error if processing fails.
2932
- */
2933
- function addTypeChecks(src, options) {
2934
- try {
2935
- const asserter = new Asserter(options);
2936
- const ast = parse(src, {
2937
- sourceType: 'module',
2938
- createParenthesizedExpressions: true
2939
- });
2940
- const out = asserter.getHeader() + asserter.toSource(ast);
2941
- return out;
2942
- } catch (e) {
2943
- console.error(e);
2944
- return `/*<addTypeChecks-error>*/${src}/*</addTypeChecks-error>*/`;
2945
2936
  }
2946
- }
2947
-
2948
- /**
2949
- * @param {object} ast - The Babel AST.
2950
- * @returns {string} String representation in JSON format for debugging/inspecting the AST.
2951
- */
2952
- function ast2json(ast) {
2953
- return JSON.stringify(ast, function (name, val) {
2954
- if (name === "loc" || name === "start" || name === "end") {
2955
- return; // remove
2956
- }
2957
-
2958
- return val; // keep
2959
- }, 2);
2960
- }
2961
-
2962
- const drop = ['loc', 'start', 'end', 'leadingComments', 'trailingComments', 'innerComments', 'innerComments', 'comments'];
2963
- /**
2964
- * @example
2965
- * setRight(ast2jsonForComparison(parseSync("/** *"))); // Close comment with / after last *
2966
- * @param {object} ast - The Babel AST.
2967
- * @returns {string} String representation in JSON format for debugging/inspecting the AST.
2968
- */
2969
- function ast2jsonForComparison(ast) {
2970
- return JSON.stringify(ast, function (name, val) {
2971
- if (name === 'trailingComma' || name === 'parenStart') {
2972
- return 'offset removed for better comparison';
2973
- }
2974
- if (drop.includes(name)) {
2975
- return undefined; // remove
2937
+ /**
2938
+ * Retrieves the name of a parameter from a Babel AST node.
2939
+ *
2940
+ * This function expects a node representing a function parameter and attempts to extract
2941
+ * the parameter's name directly or from an AssignmentPattern.
2942
+ *
2943
+ * @param {Node} param - The AST node representing the function parameter from which to extract the name.
2944
+ * @returns {string} The name of the parameter as a string, or the parameter's source code if the extraction fails.
2945
+ */
2946
+ getNameOfParam(param) {
2947
+ if (param.type === 'Identifier') {
2948
+ return param.name;
2949
+ } else if (param.type === 'AssignmentPattern') {
2950
+ if (param.left.type === 'Identifier') {
2951
+ return param.left.name;
2952
+ }
2976
2953
  }
2977
-
2978
- return val; // keep
2979
- }, 2);
2980
- }
2981
-
2982
- /**
2983
- * A roundtrip between code -> AST -> code to validate Stringifier.
2984
- * @param {string} code - The code.
2985
- * @returns {string | undefined} The new and once parsed and stringified code.
2986
- */
2987
- function code2ast2code(code) {
2988
- const stringifier = new Stringifier();
2989
- const ast = parse(code, {
2990
- sourceType: 'module'
2991
- });
2992
- if (!ast) {
2993
- return;
2994
- }
2995
- const out = stringifier.toSource(ast);
2996
- return out;
2997
- }
2998
-
2999
- /**
3000
- * @param {string} left - Left source code.
3001
- * @param {string} right - Right source code.
3002
- * @returns {boolean} Whether source codes are identical on the AST level.
3003
- */
3004
- function compareAST(left, right) {
3005
- const l = parse(left, {
3006
- sourceType: 'module'
3007
- });
3008
- const r = parse(right, {
3009
- sourceType: 'module'
3010
- });
3011
- const ljson = ast2jsonForComparison(l);
3012
- const rjson = ast2jsonForComparison(r);
3013
- const test = ljson === rjson;
3014
- return test;
3015
- }
3016
-
3017
- /**
3018
- * Transforms a type string into a structured type representation.
3019
- *
3020
- * This function parses a given type string and converts it into a TypeScript
3021
- * Abstract Syntax Tree (AST), then uses that AST to return a structured type
3022
- * representation that can be further utilized or interpreted.
3023
- *
3024
- * @todo Better handling of weird case: Array<>
3025
- * @example
3026
- * const {expandType} = await import("./src-transpiler/expandType.mjs");
3027
- * expandType('[string, Array|AnyTypedArray, number[]]|[ONNXTensor]');
3028
- * expandType('(123) '); // Outputs: '123'
3029
- * expandType(' ( ( 123 ) ) '); // Outputs: '123'
3030
- * expandType('Array<number> '); // Outputs: {type: 'array', elementType: 'number'}
3031
- * expandType('Array<(123) > '); // Outputs: {type: 'array', elementType: '123'}
3032
- * expandType('Array<"abc" | 123> '); // Outputs: {type: 'array', elementType: {type: 'union', members: ['"abc"', '123']}}
3033
- * expandType(' (string ) |(number ) '); // Outputs: {type: 'union', members: [ 'string', 'number']}
3034
- * expandType(' "apples" | ( "bananas") '); // Outputs: {type: 'union', members: [ '"apples"', '"bananas"']}
3035
- * expandType('123? '); // Outputs: {"type":"union","members":["123","null"]}
3036
- * expandType('123|null '); // Outputs: {"type":"union","members":["123","null"]}
3037
- * expandType('Map<string, any> '); // Outputs:
3038
- * expandType('typeof Number '); // Outputs:
3039
- * @param {string} type - The type string to be expanded into a structured representation.
3040
- * @todo Share type with expandTypeBabelTS and expandTypeDepFree
3041
- * @returns {string | {type: string, [key: string]: any} | undefined} The structured type
3042
- * representation obtained from parsing and converting the provided type string.
3043
- */
3044
- function expandType(type) {
3045
- const ast = parseType(type);
3046
- return toSourceTS(ast);
3047
- }
3048
- /**
3049
- * @todo I want to use for example: import('typescript').Node
3050
- * But the TS types make no sense to me so far ... need to investigate more.
3051
- * @typedef TypeScriptType
3052
- * @property {object[]|undefined} typeArguments - The type arguments.
3053
- * @property {import('typescript').Node} typeName - The type name.
3054
- * @property {number} kind - The kind for `ts.SyntaxKind[kind]`.
3055
- */
3056
- /**
3057
- * @param {string} str - The type string.
3058
- * @returns {TypeScriptType} - The node containing all the information about the input type string.
3059
- */
3060
- function parseType(str) {
3061
- // TS doesn't like ... notation in this context
3062
- if (str.startsWith('...')) {
3063
- str = str.slice(3); // remove dots
3064
- str += '[]'; // turn into array
2954
+ debugger;
2955
+ this.warn("unable to extra name from param in specified way - may contain too much information");
2956
+ return this.toSource(param);
3065
2957
  }
3066
- // type tmp = (...string) => 123; to have a function context
3067
- str = `type tmp = ${str};`;
3068
- const ast = ts.createSourceFile('repl.ts', str, ts.ScriptTarget.Latest, true /*setParentNodes*/);
3069
- return ast.statements[0].type;
3070
- }
3071
- /**
3072
- * Converts a TypeScript AST node to a source string representation or to an intermediate object describing the type.
3073
- *
3074
- * This function handles various TypeScript AST node types and converts them into a string
3075
- * or an object representing the type.
3076
- *
3077
- * @param {TypeScriptType} node - The TypeScript AST node to convert.
3078
- * @returns {string | number | boolean | {type: string, [key: string]: any} | undefined} The source string/number,
3079
- * or an object with type information based on the node, or `undefined` if the node kind is not handled.
3080
- */
3081
- function toSourceTS(node) {
3082
- const {
3083
- typeArguments,
3084
- typeName
3085
- } = node;
3086
- const kind_ = ts.SyntaxKind[node.kind];
3087
- const {
3088
- AnyKeyword,
3089
- ArrayType,
3090
- BooleanKeyword,
3091
- FunctionType,
3092
- Identifier,
3093
- IntersectionType,
3094
- JSDocAllType,
3095
- LastTypeNode,
3096
- LiteralType,
3097
- NullKeyword,
3098
- NumberKeyword,
3099
- NumericLiteral,
3100
- ObjectKeyword,
3101
- Parameter,
3102
- ParenthesizedType,
3103
- PropertySignature,
3104
- StringKeyword,
3105
- StringLiteral,
3106
- ThisType,
3107
- TupleType,
3108
- TypeLiteral,
3109
- TypeReference,
3110
- UndefinedKeyword,
3111
- UnionType,
3112
- JSDocNullableType,
3113
- TrueKeyword,
3114
- FalseKeyword,
3115
- VoidKeyword,
3116
- UnknownKeyword,
3117
- NeverKeyword,
3118
- BigIntKeyword,
3119
- BigIntLiteral,
3120
- ConditionalType,
3121
- IndexedAccessType,
3122
- RestType,
3123
- TypeQuery,
3124
- // parseType('typeof Number')
3125
- TypeOperator,
3126
- // parseType('keyof typeof obj')
3127
- KeyOfKeyword,
3128
- // "operator" key in TypeOperator node
3129
- ConstructorType,
3130
- // parseType('new (...args: any[]) => any');
3131
- NamedTupleMember,
3132
- MappedType,
3133
- // parseType('{[K in TaskType]: InstanceType<typeof SUPPORTED_TASKS[K]["pipeline"]>}')
3134
- TypeParameter // Basically K and TaskType of MappedType
3135
- } = ts.SyntaxKind;
3136
- // console.log({typeArguments, typeName, kind_, node});
3137
- switch (node.kind) {
3138
- case BigIntKeyword:
3139
- return {
3140
- type: 'bigint'
3141
- };
3142
- case BigIntLiteral:
3143
- const literal = node.text.slice(0, -1); // Remove the "n"
3144
- return {
3145
- type: 'bigint',
3146
- literal
3147
- };
3148
- case ConditionalType:
3149
- // Keys on node:
3150
- // ['pos', 'end', 'flags', 'modifierFlagsCache', 'transformFlags', 'parent', 'kind', 'checkType',
3151
- // 'extendsType', 'trueType', 'falseType', 'locals', 'nextContainer']
3152
- const checkType = toSourceTS(node.checkType);
3153
- const extendsType = toSourceTS(node.extendsType);
3154
- const trueType = toSourceTS(node.trueType);
3155
- const falseType = toSourceTS(node.falseType);
2958
+ statsReset() {
2959
+ Object.values(this.stats).forEach(statReset);
2960
+ }
2961
+ statsPrint() {
2962
+ console.table(this.stats);
2963
+ }
2964
+ /**
2965
+ * Retrieves statistical information for a given Babel AST node of this instance.
2966
+ *
2967
+ * @param {Node} node - The Babel AST node for which the statistical data is retrieved.
2968
+ * @returns {Stat} An object containing the statistical data for the specified node. If the
2969
+ * node type is unhandled, defaults to returning a dummy object with 'checked' and 'unchecked'
2970
+ * properties both set to 0.
2971
+ */
2972
+ getStatsForNode(node) {
2973
+ const {
2974
+ stats
2975
+ } = this;
2976
+ const type = nodeIsFunction(node) ? node.type : this.parentType;
2977
+ if (type === 'ClassMethod') {
2978
+ const parent = /** @type {ClassMethod} */
2979
+ this.parent;
2980
+ const {
2981
+ kind
2982
+ } = parent;
2983
+ return stats[`ClassMethod#${kind}`];
2984
+ } else if (type === 'ClassPrivateMethod') {
2985
+ const parent = /** @type {ClassPrivateMethod} */
2986
+ this.parent;
2987
+ const {
2988
+ kind
2989
+ } = parent;
2990
+ return stats[`ClassPrivateMethod#${kind}`];
2991
+ }
2992
+ const stat = stats[type];
2993
+ if (!stat) {
2994
+ this.warn("getStatsForNode> dummy, but unhandled... fix for node type", node);
3156
2995
  return {
3157
- type: 'condition',
3158
- checkType,
3159
- extendsType,
3160
- trueType,
3161
- falseType
2996
+ checked: 0,
2997
+ unchecked: 0
3162
2998
  };
3163
- case ConstructorType:
3164
- {
3165
- const _parameters = node.parameters.map(toSourceTS);
3166
- const _ret = toSourceTS(node.type);
3167
- return {
3168
- type: 'new',
3169
- parameters: _parameters,
3170
- ret: _ret
3171
- };
2999
+ }
3000
+ return stat;
3001
+ }
3002
+ /**
3003
+ * Checks if a provided Babel AST node has a parameter with the given name.
3004
+ *
3005
+ * This function will look at the node's parameters if available and determine whether
3006
+ * one of them matches the provided name. Supports various parameter types such as Identifiers
3007
+ * and AssignmentPatterns.
3008
+ *
3009
+ * @param {Node} node - The Babel AST node to inspect. If it's a BlockStatement, the parent node is used instead.
3010
+ * @param {string} name - The name of the parameter to look for within the node's parameters.
3011
+ * @returns {boolean} True if the node has a parameter with the given name; false otherwise.
3012
+ */
3013
+ nodeHasParamName(node, name) {
3014
+ if (node.type === 'BlockStatement') {
3015
+ node = this.parent;
3016
+ }
3017
+ const {
3018
+ params
3019
+ } = node;
3020
+ if (!params) {
3021
+ this.warn("nodeHasParamName> Expected params for", {
3022
+ node,
3023
+ name
3024
+ });
3025
+ return false;
3026
+ }
3027
+ return params.some(node => {
3028
+ const {
3029
+ type
3030
+ } = node;
3031
+ if (type === "AssignmentPattern") {
3032
+ const {
3033
+ left
3034
+ } = node;
3035
+ console.assert(left.type === 'Identifier' || left.type === 'ObjectPattern' || left.type === 'ArrayPattern' || left.type === 'AssignmentPattern', 'Expected Identifier or ObjectPattern');
3036
+ return left.name === name;
3037
+ } else if (type === 'Identifier') {
3038
+ return node.name === name;
3039
+ } else if (type === 'ArrayPattern' || type === 'ObjectPattern' || type === 'RestElement') {
3040
+ return false;
3041
+ }
3042
+ const _ = new Stringifier();
3043
+ const code = _.toSource(node);
3044
+ console.log("Unknown type to test params for", type, code);
3045
+ return false;
3046
+ });
3047
+ }
3048
+ /**
3049
+ * Generates a string containing type checks for a given Babel AST node based on associated JSDoc information.
3050
+ *
3051
+ * This function analyzes the node and its JSDoc annotations to construct runtime type
3052
+ * check expressions. It handles various parameter patterns and outputs code that performs
3053
+ * actual type assertions. If a node does not correspond to any known or supported pattern,
3054
+ * it returns an empty string.
3055
+ *
3056
+ * @override
3057
+ * @param {Node} node - The Babel AST node for which to generate type checks.
3058
+ * @returns {string} A string of code with type check assertions, based on the JSDoc comments associated with the given node.
3059
+ */
3060
+ generateTypeChecks(node) {
3061
+ const {
3062
+ parent
3063
+ } = this;
3064
+ if (node.type === 'BlockStatement' && !nodeIsFunction(parent)) {
3065
+ return '';
3066
+ }
3067
+ const jsdoc = this.getJSDoc(node);
3068
+ // return '// ' + JSON.stringify(jsdoc) + '\n';
3069
+ const stat = this.getStatsForNode(node);
3070
+ if (!jsdoc) {
3071
+ stat.unchecked++;
3072
+ return '';
3073
+ }
3074
+ stat.checked++;
3075
+ const {
3076
+ spaces
3077
+ } = this;
3078
+ let out = '';
3079
+ let first = true;
3080
+ const loc = this.getName(node);
3081
+ if (this.ignoreLocations.includes(loc)) {
3082
+ return '// IGNORE RTI TYPE VALIDATIONS, KNOWN ISSUES\n';
3083
+ }
3084
+ //out += `${spaces}/*${spaces} node.type=${node.type}\n${spaces}
3085
+ // ${JSON.stringify(jsdoc)}\n${parent}\n${spaces}*/\n`;
3086
+ for (let name in jsdoc) {
3087
+ const type = jsdoc[name];
3088
+ const hasParam = this.nodeHasParamName(node, name);
3089
+ if (!hasParam) {
3090
+ let testNode = node;
3091
+ if (node.type === 'BlockStatement') {
3092
+ testNode = this.parent;
3093
+ }
3094
+ const paramIndex = Object.keys(jsdoc).findIndex(_ => _ === name);
3095
+ const param = testNode.params[paramIndex];
3096
+ if (param) {
3097
+ const isObjectPattern = param.type === 'ObjectPattern';
3098
+ const isArrayPattern = param.type === 'ArrayPattern';
3099
+ const isSupportedPattern = isObjectPattern || isArrayPattern;
3100
+ // There are four kinds of patterns:
3101
+ // ObjectPattern:
3102
+ // function test({x = 123}) {return x;} test({x: 456});
3103
+ // ArrayPattern:
3104
+ // function test([x = 123]) {return x;}; test([456]);
3105
+ // AssignmentPattern made up of ObjectPattern:
3106
+ // function test({x = 123} = {}) {return x;} test();
3107
+ // AssignmentPattern made up of ArrayPattern:
3108
+ // function test([x = 123] = []) {return x;} test();
3109
+ if (isSupportedPattern) {
3110
+ // The name doesn't matter any longer, because any pattern inherently
3111
+ // drops the identifier from the AST. But we can access it
3112
+ // via arguments[paramIndex] anyway.
3113
+ name = `arguments[${paramIndex}]`;
3114
+ } else if (param.type === 'AssignmentPattern') {
3115
+ const _loc = this.getName(node);
3116
+ if (param.left.type === 'ArrayPattern' && type.type === 'array') {
3117
+ // Add a type assertion for each element of the ArrayPattern
3118
+ for (const element of param.left.elements) {
3119
+ if (element.type !== 'Identifier') {
3120
+ this.warn('Only Identifier case handled right now');
3121
+ continue;
3122
+ }
3123
+ const _t = JSON.stringify(type.elementType, null, 2).replaceAll('\n', '\n' + spaces);
3124
+ out += `${spaces}if (!inspectType(${element.name}, ${_t}, '${_loc}', '${name}')) {\n`;
3125
+ out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3126
+ }
3127
+ continue;
3128
+ } else if (param.left.type === 'ObjectPattern' && type.type === 'object') {
3129
+ // Add a type assertion for each property of the ObjectPattern
3130
+ for (const property of param.left.properties) {
3131
+ if (property.key.type !== 'Identifier') {
3132
+ this.warn('ObjectPattern> Only Identifier case handled right now');
3133
+ continue;
3134
+ }
3135
+ const keyName = property.key.name;
3136
+ if (type.type !== 'object' || !type.properties) {
3137
+ this.warn("missing subtype information in JSDoc> in type", JSON.stringify(type, null, 2), "for ObjectPattern:", this.toSource(property).trim());
3138
+ continue;
3139
+ }
3140
+ const subType = type.properties[keyName];
3141
+ if (!subType) {
3142
+ this.warn("missing subtype information in JSDoc");
3143
+ continue;
3144
+ }
3145
+ const _t2 = JSON.stringify(subType, null, 2).replaceAll('\n', '\n' + spaces);
3146
+ out += `${spaces}if (!inspectType(${keyName}, ${_t2}, '${_loc}', '${name}')) {\n`;
3147
+ out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3148
+ }
3149
+ continue;
3150
+ }
3151
+ this.warn(`generateTypeChecks> ${_loc}> todo implement`, `AssignmentPattern for parameter ${name}`);
3152
+ continue;
3153
+ }
3154
+ } else {
3155
+ const _loc2 = this.getName(node);
3156
+ this.warn(`generateTypeChecks> ${_loc2}> Missing param: ${name}`);
3157
+ continue;
3158
+ }
3172
3159
  }
3173
- case FunctionType:
3174
- const parameters = node.parameters.map(toSourceTS);
3175
- return {
3176
- type: 'function',
3177
- parameters
3178
- };
3179
- case IndexedAccessType:
3180
- const index = toSourceTS(node.indexType);
3181
- const object = toSourceTS(node.objectType);
3182
- return {
3183
- type: 'indexedAccess',
3184
- index,
3185
- object
3186
- };
3187
- case RestType:
3188
- const annotation = toSourceTS(node.type);
3189
- return {
3190
- type: 'rest',
3191
- annotation
3192
- };
3193
- case JSDocNullableType:
3194
- const t = toSourceTS(node.type);
3195
- return {
3196
- type: 'union',
3197
- members: [t, 'null']
3198
- };
3199
- case MappedType:
3200
- {
3201
- const result = toSourceTS(node.type);
3202
- const parameter = node.typeParameter;
3203
- if (parameter.kind === TypeParameter) {
3204
- // For example: {[K in TaskType]: InstanceType etc.
3205
- const iterable = toSourceTS(parameter.constraint); // TaskType
3206
- const element = toSourceTS(parameter.name); // K
3207
- return {
3208
- type: 'mapping',
3209
- iterable,
3210
- element,
3211
- result
3212
- };
3160
+ let t = JSON.stringify(type, null, 2).replaceAll('\n', '\n' + spaces);
3161
+ if (type === 'this') {
3162
+ const classDecl = this.findParentOfType(node, 'ClassDeclaration');
3163
+ if (!(classDecl != null && classDecl.id)) {
3164
+ this.warn('generateTypeChecks> !classDecl?.id');
3213
3165
  }
3214
- console.warn("MappedType: expected TypeParameter");
3215
- return 'transpiler-error';
3166
+ t = '"' + this.toSource(classDecl.id) + '"';
3216
3167
  }
3217
- // todo work out more: const jsdoc = `(...a: ...number) => 123
3218
- // TS even thinks it's two parameters... just go for array/[]
3219
- case Parameter:
3220
- const type = node.type ? toSourceTS(node.type) : 'any';
3221
- const name = toSourceTS(node.name);
3222
- const ret = {
3223
- type,
3224
- name
3225
- };
3226
- if (node.dotDotDotToken) {
3227
- return {
3228
- type: 'array',
3229
- elementType: ret
3230
- };
3168
+ const _loc3 = this.getName(node);
3169
+ let prevCheck = '';
3170
+ // JSDoc doesn't support multiple function signatures yet, but this is
3171
+ // exactly what we would need to deal with ObjectPool'ing
3172
+ if (_loc3 === 'ContactPoint#constructor' || _loc3 === 'ContactResult#constructor' || _loc3 === 'SingleContactResult#constructor') {
3173
+ prevCheck = 'arguments.length !== 0 && ';
3231
3174
  }
3232
- return ret;
3233
- case TypeQuery:
3234
- const argument = toSourceTS(node.exprName);
3235
- return {
3236
- type: 'typeof',
3237
- argument
3238
- };
3239
- case TypeOperator:
3240
- if (node.operator === KeyOfKeyword) {
3241
- const _argument = toSourceTS(node.type);
3242
- return {
3243
- type: 'keyof',
3244
- argument: _argument
3245
- };
3175
+ if (first) {
3176
+ out += '\n';
3177
+ first = false;
3246
3178
  }
3247
- console.warn("unimplemented TypeOperator", node);
3248
- case TypeReference:
3249
- {
3250
- if ((typeName.text === 'Object' || typeName.text === 'Record') && (typeArguments == null ? void 0 : typeArguments.length) === 2) {
3251
- return {
3252
- type: 'record',
3253
- key: toSourceTS(typeArguments[0]),
3254
- val: toSourceTS(typeArguments[1])
3255
- };
3256
- } else if (typeName.text === 'Object' && (!typeArguments || (typeArguments == null ? void 0 : typeArguments.length) === 0)) {
3257
- return {
3258
- type: 'object',
3259
- properties: {}
3260
- };
3261
- } else if (typeName.text === 'Map' && (typeArguments == null ? void 0 : typeArguments.length) === 2) {
3262
- const key = toSourceTS(typeArguments[0]);
3263
- const val = toSourceTS(typeArguments[1]);
3264
- return {
3265
- type: 'map',
3266
- key,
3267
- val
3268
- };
3269
- } else if (typeName.text === 'Array' && (typeArguments == null ? void 0 : typeArguments.length) === 1) {
3270
- const elementType = toSourceTS(typeArguments[0]);
3271
- return {
3272
- type: 'array',
3273
- elementType
3274
- };
3275
- } else if (typeName.text === 'Promise' && (typeArguments == null ? void 0 : typeArguments.length) === 1) {
3276
- const elementType = toSourceTS(typeArguments[0]);
3277
- return {
3278
- type: 'promise',
3279
- elementType
3280
- };
3281
- } else if (typeName.text === 'Set' && (typeArguments == null ? void 0 : typeArguments.length) === 1) {
3282
- const elementType = toSourceTS(typeArguments[0]);
3283
- return {
3284
- type: 'set',
3285
- elementType
3286
- };
3287
- } else if (typeName.text === 'Class' && (typeArguments == null ? void 0 : typeArguments.length) === 1) {
3288
- const elementType = toSourceTS(typeArguments[0]);
3289
- return {
3290
- type: 'class',
3291
- elementType
3292
- };
3293
- }
3294
- if (!typeArguments) {
3295
- return typeName.getText();
3296
- }
3297
- const _name = typeName.text;
3298
- const args = typeArguments.map(toSourceTS);
3299
- return {
3300
- type: 'reference',
3301
- name: _name,
3302
- args
3303
- };
3179
+ out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${_loc3}', '${name}')) {\n`;
3180
+ out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3181
+ }
3182
+ return out;
3183
+ }
3184
+ /**
3185
+ * @param {Node} node - The Babel AST node.
3186
+ * @returns {string} Best possible human-readable name of given node.
3187
+ */
3188
+ getNameForFunctionExpression(node) {
3189
+ const objectProperty = this.findParentOfType(node, 'ObjectProperty');
3190
+ if (objectProperty) {
3191
+ // See good-old-es5.mjs example for a test case
3192
+ // TODO: Make an even better name based on Object.assign(ScopeSpace.prototype
3193
+ // Ideally we would figure out the name: ScopeSpace#resolve
3194
+ // Currently we only find "resolve" (still better than 'unnamed function expression'...)
3195
+ return this.toSource(objectProperty.key);
3196
+ }
3197
+ const expressionStatement = this.findParentOfType(node, 'ExpressionStatement');
3198
+ if (expressionStatement) {
3199
+ // There are many kinds of expressions
3200
+ // type Expression = ArrayExpression | AssignmentExpression | BinaryExpression | CallExpression | ...
3201
+ const {
3202
+ left
3203
+ } = expressionStatement.expression;
3204
+ if (left) {
3205
+ return this.toSource(left);
3304
3206
  }
3305
- case StringKeyword:
3306
- return node.getText();
3307
- case NumberKeyword:
3308
- return node.getText();
3309
- case NamedTupleMember:
3310
- return toSourceTS(node.type);
3311
- case IntersectionType:
3312
- {
3313
- const _members = node.types.map(toSourceTS);
3314
- return {
3315
- type: 'intersection',
3316
- members: _members
3317
- };
3207
+ console.warn("Asserter#getNameForFunctionExpression> expression without left");
3208
+ }
3209
+ return 'unnamed function expression';
3210
+ }
3211
+ /**
3212
+ * @param {Node} node - The Babel AST node.
3213
+ * @returns {string} Stringification of the node.
3214
+ */
3215
+ getName(node) {
3216
+ const toSource = this.toSource.bind(this);
3217
+ if (node.type === 'BlockStatement') {
3218
+ node = this.parent;
3219
+ }
3220
+ const {
3221
+ type,
3222
+ key,
3223
+ id
3224
+ } = node;
3225
+ switch (type) {
3226
+ case 'FunctionDeclaration':
3227
+ return toSource(id);
3228
+ case 'ClassMethod':
3229
+ case 'ClassPrivateMethod':
3230
+ const classDecl = this.parents.findLast(_ => _.type === 'ClassDeclaration');
3231
+ let out = '';
3232
+ if (classDecl) {
3233
+ out += toSource(classDecl.id) + '#';
3234
+ }
3235
+ out += toSource(key);
3236
+ return out;
3237
+ case 'FunctionExpression':
3238
+ if (id) {
3239
+ return toSource(id);
3240
+ }
3241
+ return this.getNameForFunctionExpression(node);
3242
+ case 'ArrowFunctionExpression':
3243
+ const parent = this.findParentOfType(node, 'VariableDeclarator');
3244
+ if (parent) {
3245
+ return toSource(parent.id);
3246
+ }
3247
+ return 'getName> missing parent for ' + node.type;
3248
+ case 'ObjectMethod':
3249
+ return toSource(key);
3250
+ default:
3251
+ this.warn('getName> unhandled type', type, 'for', node);
3252
+ return '/*MISSING*/';
3253
+ }
3254
+ }
3255
+ /**
3256
+ * @override
3257
+ * @param {import("@babel/types").BinaryExpression} node - The Babel AST node.
3258
+ * @returns {string} Stringification of the node.
3259
+ */
3260
+ BinaryExpression(node) {
3261
+ if (!this.validateDivision) {
3262
+ return super.BinaryExpression(node);
3263
+ }
3264
+ const {
3265
+ left,
3266
+ operator,
3267
+ right
3268
+ } = node;
3269
+ const left_ = this.toSource(left);
3270
+ const right_ = this.toSource(right);
3271
+ if (operator === '/') {
3272
+ return `validateDivision(${left_}, ${right_})`;
3273
+ }
3274
+ return `${left_} ${operator} ${right_}`;
3275
+ }
3276
+ /**
3277
+ * @override
3278
+ * @param {import("@babel/types").File} node - The Babel AST node.
3279
+ * @returns {string} Stringification of the node.
3280
+ */
3281
+ File(node) {
3282
+ // @todo figure out why errors is in Babel node and not in @babel/types...
3283
+ const {
3284
+ /*errors,*/program,
3285
+ comments
3286
+ } = node;
3287
+ if (comments) {
3288
+ for (const comment of comments) {
3289
+ const warn = this.warn.bind(this);
3290
+ parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
3318
3291
  }
3319
- case TupleType:
3320
- const elements = node.elements.map(toSourceTS);
3321
- return {
3322
- type: 'tuple',
3323
- elements
3324
- };
3325
- case UnionType:
3326
- const members = node.types.map(toSourceTS);
3327
- return {
3328
- type: 'union',
3329
- members
3330
- };
3331
- case TypeLiteral:
3332
- const properties = {};
3333
- node.members.forEach(member => {
3334
- const name = toSourceTS(member.name);
3335
- const type = toSourceTS(member.type);
3336
- properties[name] = type;
3337
- });
3338
- return {
3339
- type: 'object',
3340
- properties
3341
- };
3342
- case PropertySignature:
3343
- console.warn('toSourceTS> should not happen, handled by TypeLiteral directly');
3344
- return `${toSourceTS(node.name)}: ${toSourceTS(node.type)}`;
3345
- case Identifier:
3346
- return node.text;
3347
- case ArrayType:
3348
- {
3349
- const elementType = toSourceTS(node.elementType);
3350
- return {
3351
- type: 'array',
3352
- elementType
3353
- };
3292
+ }
3293
+ //console.log("this.typedefs", this.typedefs);
3294
+ let out = '';
3295
+ for (const name in this.typedefs) {
3296
+ const typedef = this.typedefs[name];
3297
+ const json = JSON.stringify(typedef, null, 2);
3298
+ out += `registerTypedef('${name}', ${json});\n`;
3299
+ }
3300
+ const code = this.toSource(program) + '\n';
3301
+ out += code;
3302
+ return out;
3303
+ }
3304
+ /**
3305
+ * @override
3306
+ * @param {import("@babel/types").VariableDeclaration} node - The Babel AST node.
3307
+ * @returns {string} Stringification of the node.
3308
+ */
3309
+ VariableDeclaration(node) {
3310
+ const {
3311
+ declarations
3312
+ } = node;
3313
+ let ret = super.VariableDeclaration(node);
3314
+ for (const {
3315
+ id
3316
+ } of declarations) {
3317
+ const name = this.toSource(id);
3318
+ if (requiredTypeofs[name] === 'missing') {
3319
+ ret += `\n${this.spaces}registerVariable('${name}', ${name});\n`;
3320
+ requiredTypeofs[name] = 'found';
3321
+ } else if (requiredTypeofs[name] === 'found') {
3322
+ console.warn(`Already registered variable named ${name} for typeof validation`);
3354
3323
  }
3355
- case LiteralType:
3356
- return toSourceTS(node.literal);
3357
- case AnyKeyword:
3358
- case BooleanKeyword:
3359
- // ts.SyntaxKind[parseType("*").kind] === 'JSDocAllType'
3360
- case JSDocAllType:
3361
- case NullKeyword:
3362
- case StringLiteral:
3363
- case ThisType:
3364
- case UndefinedKeyword:
3365
- case VoidKeyword:
3366
- case UnknownKeyword:
3367
- case NeverKeyword:
3368
- return node.getText();
3369
- case TrueKeyword:
3370
- return true;
3371
- case FalseKeyword:
3372
- return false;
3373
- case NumericLiteral:
3374
- return Number(node.getText());
3375
- case ObjectKeyword:
3376
- return {
3377
- type: 'object',
3378
- properties: {}
3379
- };
3380
- case ParenthesizedType:
3381
- // fall-through for parentheses
3382
- return toSourceTS(node.type);
3383
- case LastTypeNode:
3384
- return toSourceTS(node.qualifier);
3385
- default:
3386
- // const test = {};
3387
- // Object.entries(ts.SyntaxKind).forEach(([name, id]) => {
3388
- // test[id] = (test[id] || []);
3389
- // test[id].push(name);
3390
- // });
3391
- // console.log(test);
3392
- console.warn('toSourceTS> unhandled kind - make sure to understand you cannot reverse TS enums');
3393
- console.warn('if they contain range aliases, for example:');
3394
- console.warn('ts.SyntaxKind.NumericLiteral === ts.SyntaxKind.FirstLiteralToken');
3395
- console.warn('so this "kind" could be wrong, but requires handling anyway:', kind_, node);
3396
- debugger;
3324
+ }
3325
+ return ret;
3326
+ }
3327
+ }
3328
+
3329
+ /**
3330
+ * Simple facade which does all the processing. Processes the input
3331
+ * source string, adding runtime type checks based on JSDoc comments.
3332
+ *
3333
+ * This function takes JavaScript source code as input, parses it to an AST, traverses the
3334
+ * AST to find type annotations in JSDoc comments, and generates appropriate runtime type
3335
+ * assertions. These are then inserted into the source, producing a new version of the code
3336
+ * that includes runtime type checking based on the original JSDoc annotations.
3337
+ *
3338
+ * @param {string} src - The input source code containing JSDoc comments to be processed
3339
+ * for type checks.
3340
+ * @param {import('./Asserter.mjs').Options} [options] - Configuration options that dictate
3341
+ * how the processing is performed.
3342
+ * @returns {string} The transformed source code with inserted runtime type checks, or the
3343
+ * original source code commented with an error if processing fails.
3344
+ */
3345
+ function addTypeChecks(src, options) {
3346
+ try {
3347
+ const asserter = new Asserter(options);
3348
+ const ast = parse(src, {
3349
+ sourceType: 'module',
3350
+ createParenthesizedExpressions: true
3351
+ });
3352
+ const out = asserter.getHeader() + asserter.toSource(ast);
3353
+ return out;
3354
+ } catch (e) {
3355
+ console.error(e);
3356
+ return `/*<addTypeChecks-error>*/${src}/*</addTypeChecks-error>*/`;
3357
+ }
3358
+ }
3359
+
3360
+ /**
3361
+ * @param {object} ast - The Babel AST.
3362
+ * @returns {string} String representation in JSON format for debugging/inspecting the AST.
3363
+ */
3364
+ function ast2json(ast) {
3365
+ return JSON.stringify(ast, function (name, val) {
3366
+ if (name === "loc" || name === "start" || name === "end") {
3367
+ return; // remove
3368
+ }
3369
+
3370
+ return val; // keep
3371
+ }, 2);
3372
+ }
3373
+
3374
+ const drop = ['loc', 'start', 'end', 'leadingComments', 'trailingComments', 'innerComments', 'innerComments', 'comments'];
3375
+ /**
3376
+ * @example
3377
+ * setRight(ast2jsonForComparison(parseSync("/** *"))); // Close comment with / after last *
3378
+ * @param {object} ast - The Babel AST.
3379
+ * @returns {string} String representation in JSON format for debugging/inspecting the AST.
3380
+ */
3381
+ function ast2jsonForComparison(ast) {
3382
+ return JSON.stringify(ast, function (name, val) {
3383
+ if (name === 'trailingComma' || name === 'parenStart') {
3384
+ return 'offset removed for better comparison';
3385
+ }
3386
+ if (drop.includes(name)) {
3387
+ return undefined; // remove
3388
+ }
3389
+
3390
+ return val; // keep
3391
+ }, 2);
3392
+ }
3393
+
3394
+ /**
3395
+ * A roundtrip between code -> AST -> code to validate Stringifier.
3396
+ * @param {string} code - The code.
3397
+ * @returns {string | undefined} The new and once parsed and stringified code.
3398
+ */
3399
+ function code2ast2code(code) {
3400
+ const stringifier = new Stringifier();
3401
+ const ast = parse(code, {
3402
+ sourceType: 'module'
3403
+ });
3404
+ if (!ast) {
3405
+ return;
3397
3406
  }
3407
+ const out = stringifier.toSource(ast);
3408
+ return out;
3409
+ }
3410
+
3411
+ /**
3412
+ * @param {string} left - Left source code.
3413
+ * @param {string} right - Right source code.
3414
+ * @returns {boolean} Whether source codes are identical on the AST level.
3415
+ */
3416
+ function compareAST(left, right) {
3417
+ const l = parse(left, {
3418
+ sourceType: 'module'
3419
+ });
3420
+ const r = parse(right, {
3421
+ sourceType: 'module'
3422
+ });
3423
+ const ljson = ast2jsonForComparison(l);
3424
+ const rjson = ast2jsonForComparison(r);
3425
+ const test = ljson === rjson;
3426
+ return test;
3398
3427
  }
3399
3428
 
3400
3429
  /**
@@ -3638,4 +3667,4 @@ function toSourceBabelTS(node) {
3638
3667
  }
3639
3668
  }
3640
3669
 
3641
- export { Asserter, Stringifier, addTypeChecks, ast2json, ast2jsonForComparison, code2ast2code, compareAST, expandType, expandTypeBabelTS, expandTypeDepFree, extractCurlyContent, extractNameAndOptionality, nodeIsFunction, parseJSDoc, parseJSDocSetter, parseJSDocTypedef, parseType, parseTypeBabelTS, simplifyType, toSourceBabelTS, toSourceTS, trimEndSpaces };
3670
+ export { Asserter, Stringifier, addTypeChecks, ast2json, ast2jsonForComparison, code2ast2code, compareAST, expandType, expandTypeBabelTS, expandTypeDepFree, extractCurlyContent, extractNameAndOptionality, nodeIsFunction, parseJSDoc, parseJSDocSetter, parseJSDocTypedef, parseType, parseTypeBabelTS, requiredTypeofs, simplifyType, toSourceBabelTS, toSourceTS, trimEndSpaces };