@runtime-type-inspector/transpiler 3.2.5 → 3.2.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 +299 -109
  2. package/index.mjs +325 -109
  3. package/package.json +2 -2
package/index.mjs CHANGED
@@ -25,24 +25,19 @@ import ts from 'typescript';
25
25
  * expandType('typeof Number '); // Outputs:
26
26
  * @param {string} type - The type string to be expanded into a structured representation.
27
27
  * @todo Share type with expandTypeBabelTS and expandTypeDepFree
28
- * @returns {string | {type: string, [key: string]: any} | undefined} The structured type
28
+ * @returns {string | number | boolean | {type: string, [key: string]: any} | undefined} The structured type
29
29
  * representation obtained from parsing and converting the provided type string.
30
30
  */
31
31
  function expandType(type) {
32
32
  const ast = parseType(type);
33
+ if (!ast) {
34
+ return 'never';
35
+ }
33
36
  return toSourceTS(ast);
34
37
  }
35
- /**
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]`.
42
- */
43
38
  /**
44
39
  * @param {string} str - The type string.
45
- * @returns {TypeScriptType} - The node containing all the information about the input type string.
40
+ * @returns {ts.TypeNode|undefined} - The node containing all the information about the input type string.
46
41
  */
47
42
  function parseType(str) {
48
43
  // TS doesn't like ... notation in this context
@@ -53,7 +48,12 @@ function parseType(str) {
53
48
  // type tmp = (...string) => 123; to have a function context
54
49
  str = `type tmp = ${str};`;
55
50
  const ast = ts.createSourceFile('repl.ts', str, ts.ScriptTarget.Latest, true /*setParentNodes*/);
56
- return ast.statements[0].type;
51
+ const firstStatement = ast.statements[0];
52
+ if (!ts.isTypeAliasDeclaration(firstStatement)) {
53
+ console.warn('parseType> Expected type alias declaration, got', firstStatement, 'instead.');
54
+ return;
55
+ }
56
+ return firstStatement.type;
57
57
  }
58
58
  /** @type {Record<string, 'missing'|'found'>} */
59
59
  const requiredTypeofs = {};
@@ -63,7 +63,7 @@ const requiredTypeofs = {};
63
63
  * This function handles various TypeScript AST node types and converts them into a string
64
64
  * or an object representing the type.
65
65
  *
66
- * @param {TypeScriptType} node - The TypeScript AST node to convert.
66
+ * @param {ts.TypeNode|ts.Identifier} node - The TypeScript AST node to convert.
67
67
  * @returns {string | number | boolean | {type: string, [key: string]: any} | undefined} The source string/number,
68
68
  * or an object with type information based on the node, or `undefined` if the node kind is not handled.
69
69
  */
@@ -75,52 +75,90 @@ function toSourceTS(node) {
75
75
  const kind_ = ts.SyntaxKind[node.kind];
76
76
  const {
77
77
  AnyKeyword,
78
+ // parseType('any' ).kind === ts.SyntaxKind.AnyKeyword && toSourceTS(parseType('any')) === 'any'
78
79
  ArrayType,
80
+ // parseType('number[]' ).kind === ts.SyntaxKind.ArrayType // todo toSourceTS(parseType('number[]')) === {type: 'array etc.
79
81
  BooleanKeyword,
82
+ // parseType("boolean" ).kind === ts.SyntaxKind.BooleanKeyword
80
83
  FunctionType,
84
+ // parseType("() => void" ).kind === ts.SyntaxKind.FunctionType
81
85
  Identifier,
86
+ // parseType("{a: 1, b: 2}" ).members[0].name.kind === ts.SyntaxKind.Identifier
82
87
  IntersectionType,
88
+ // parseType("1 & 2" ).kind === ts.SyntaxKind.IntersectionType
83
89
  JSDocAllType,
84
- LastTypeNode,
90
+ // parseType("*" ).kind === ts.SyntaxKind.JSDocAllType
91
+ ImportType,
92
+ // parseType('import("test").Test' ).kind === ts.SyntaxKind.ImportType
85
93
  LiteralType,
94
+ // parseType("123" ).kind === ts.SyntaxKind.LiteralType
86
95
  NullKeyword,
96
+ // parseType("null" ).literal.kind === ts.SyntaxKind.NullKeyword
87
97
  NumberKeyword,
98
+ // parseType("number" ).kind === ts.SyntaxKind.NumberKeyword
88
99
  NumericLiteral,
100
+ // parseType("123" ).literal.kind === ts.SyntaxKind.NumericLiteral
89
101
  ObjectKeyword,
102
+ // parseType("object" ).kind === ts.SyntaxKind.ObjectKeyword
90
103
  Parameter,
104
+ // parseType("(a) => void" ).parameters[0].kind === ts.SyntaxKind.Parameter
91
105
  ParenthesizedType,
106
+ // parseType("(SomeType)" ).kind === ts.SyntaxKind.ParenthesizedType
92
107
  PropertySignature,
108
+ // parseType("{a: 1, b: 2}" ).members[0].kind === ts.SyntaxKind.PropertySignature
93
109
  StringKeyword,
110
+ // parseType("string" ).kind === ts.SyntaxKind.StringKeyword
94
111
  StringLiteral,
112
+ // parseType("'test'" ).literal.kind === ts.SyntaxKind.StringLiteral
95
113
  ThisType,
114
+ // parseType("this" ).kind === ts.SyntaxKind.ThisType
96
115
  TupleType,
116
+ // parseType("[1, 2, 3]" ).kind === ts.SyntaxKind.TupleType
97
117
  TypeLiteral,
118
+ // parseType("{a: 1, b: 2}" ).kind === ts.SyntaxKind.TypeLiteral
98
119
  TypeReference,
120
+ // parseType("SomeOtherType" ).kind === ts.SyntaxKind.TypeReference
99
121
  UndefinedKeyword,
122
+ // parseType("undefined" ).kind === ts.SyntaxKind.UndefinedKeyword
100
123
  UnionType,
124
+ // parseType("1|2" ).kind === ts.SyntaxKind.UnionType
101
125
  JSDocNullableType,
126
+ // parseType("?lol?" ).kind === ts.SyntaxKind.JSDocNullableType
102
127
  TrueKeyword,
128
+ // parseType("true" ).literal.kind === ts.SyntaxKind.TrueKeyword
103
129
  FalseKeyword,
130
+ // parseType("false" ).literal.kind === ts.SyntaxKind.FalseKeyword
104
131
  VoidKeyword,
132
+ // parseType("void" ).kind === ts.SyntaxKind.VoidKeyword
105
133
  UnknownKeyword,
134
+ // parseType("unknown" ).kind === ts.SyntaxKind.UnknownKeyword
106
135
  NeverKeyword,
136
+ // parseType("never" ).kind === ts.SyntaxKind.NeverKeyword
107
137
  BigIntKeyword,
138
+ // parseType("bigint" ).kind === ts.SyntaxKind.BigIntKeyword
108
139
  BigIntLiteral,
140
+ // parseType("123n" ).literal.kind === ts.SyntaxKind.BigIntLiteral
109
141
  ConditionalType,
142
+ // parseType("1 extends number ? true : false").kind === ts.SyntaxKind.ConditionalType
110
143
  IndexedAccessType,
144
+ // parseType('Test[123]' ).kind === ts.SyntaxKind.IndexedAccessType
145
+ IndexSignature,
146
+ // parseType('{[n: number]: string}' ).members[0].kind === ts.SyntaxKind.IndexSignature
111
147
  RestType,
148
+ // parseType("[...number]" ).elements[0].kind === ts.SyntaxKind.RestType
112
149
  TypeQuery,
113
- // parseType('typeof Number')
150
+ // parseType('typeof Number' ).kind === ts.SyntaxKind.TypeQuery
114
151
  TypeOperator,
115
- // parseType('keyof typeof obj')
152
+ // parseType('keyof typeof obj' ).kind === ts.SyntaxKind.TypeOperator
116
153
  KeyOfKeyword,
117
- // "operator" key in TypeOperator node
154
+ // parseType('keyof typeof obj' ).operator === ts.SyntaxKind.KeyOfKeyword
118
155
  ConstructorType,
119
- // parseType('new (...args: any[]) => any');
156
+ // parseType('new (...args: any[]) => any' ).kind === ts.SyntaxKind.ConstructorType
120
157
  NamedTupleMember,
158
+ // parseType('[a: 1]' ).elements[0].kind === ts.SyntaxKind.NamedTupleMember
121
159
  MappedType,
122
- // parseType('{[K in TaskType]: InstanceType<typeof SUPPORTED_TASKS[K]["pipeline"]>}')
123
- TypeParameter // Basically K and TaskType of MappedType
160
+ // parseType('{[K in TaskType]: 123}' ).kind === ts.SyntaxKind.MappedType
161
+ TypeParameter // parseType('{[K in TaskType]: 123}' ).typeParameter.kind === ts.SyntaxKind.TypeParameter
124
162
  } = ts.SyntaxKind;
125
163
  // console.log({typeArguments, typeName, kind_, node});
126
164
  switch (node.kind) {
@@ -129,12 +167,18 @@ function toSourceTS(node) {
129
167
  type: 'bigint'
130
168
  };
131
169
  case BigIntLiteral:
170
+ if (!ts.isBigIntLiteral(node)) {
171
+ throw Error("Impossible");
172
+ }
132
173
  const literal = node.text.slice(0, -1); // Remove the "n"
133
174
  return {
134
175
  type: 'bigint',
135
176
  literal
136
177
  };
137
178
  case ConditionalType:
179
+ if (!ts.isConditionalTypeNode(node)) {
180
+ throw Error("Impossible");
181
+ }
138
182
  // Keys on node:
139
183
  // ['pos', 'end', 'flags', 'modifierFlagsCache', 'transformFlags', 'parent', 'kind', 'checkType',
140
184
  // 'extendsType', 'trueType', 'falseType', 'locals', 'nextContainer']
@@ -151,6 +195,9 @@ function toSourceTS(node) {
151
195
  };
152
196
  case ConstructorType:
153
197
  {
198
+ if (!ts.isConstructorTypeNode(node)) {
199
+ throw Error("Impossible");
200
+ }
154
201
  const _parameters = node.parameters.map(toSourceTS);
155
202
  const _ret = toSourceTS(node.type);
156
203
  return {
@@ -160,12 +207,18 @@ function toSourceTS(node) {
160
207
  };
161
208
  }
162
209
  case FunctionType:
210
+ if (!ts.isFunctionTypeNode(node)) {
211
+ throw Error("Impossible");
212
+ }
163
213
  const parameters = node.parameters.map(toSourceTS);
164
214
  return {
165
215
  type: 'function',
166
216
  parameters
167
217
  };
168
218
  case IndexedAccessType:
219
+ if (!ts.isIndexedAccessTypeNode(node)) {
220
+ throw Error("Impossible");
221
+ }
169
222
  const index = toSourceTS(node.indexType);
170
223
  const object = toSourceTS(node.objectType);
171
224
  return {
@@ -174,12 +227,18 @@ function toSourceTS(node) {
174
227
  object
175
228
  };
176
229
  case RestType:
230
+ if (!ts.isRestTypeNode(node)) {
231
+ throw Error("Impossible");
232
+ }
177
233
  const annotation = toSourceTS(node.type);
178
234
  return {
179
235
  type: 'rest',
180
236
  annotation
181
237
  };
182
238
  case JSDocNullableType:
239
+ if (!ts.isJSDocNullableType(node)) {
240
+ throw Error("Impossible");
241
+ }
183
242
  const t = toSourceTS(node.type);
184
243
  return {
185
244
  type: 'union',
@@ -187,6 +246,9 @@ function toSourceTS(node) {
187
246
  };
188
247
  case MappedType:
189
248
  {
249
+ if (!ts.isMappedTypeNode(node)) {
250
+ throw Error("Impossible");
251
+ }
190
252
  const result = toSourceTS(node.type);
191
253
  const parameter = node.typeParameter;
192
254
  if (parameter.kind === TypeParameter) {
@@ -206,6 +268,9 @@ function toSourceTS(node) {
206
268
  // todo work out more: const jsdoc = `(...a: ...number) => 123
207
269
  // TS even thinks it's two parameters... just go for array/[]
208
270
  case Parameter:
271
+ if (!ts.isParameter(node)) {
272
+ throw Error("Impossible");
273
+ }
209
274
  const type = node.type ? toSourceTS(node.type) : 'any';
210
275
  const name = toSourceTS(node.name);
211
276
  const ret = {
@@ -220,6 +285,9 @@ function toSourceTS(node) {
220
285
  }
221
286
  return ret;
222
287
  case TypeQuery:
288
+ if (!ts.isTypeQueryNode(node)) {
289
+ throw Error("Impossible");
290
+ }
223
291
  const argument = toSourceTS(node.exprName);
224
292
  // Notify Asserter class that we have to register variables with this name
225
293
  if (!requiredTypeofs[argument]) {
@@ -230,6 +298,9 @@ function toSourceTS(node) {
230
298
  argument
231
299
  };
232
300
  case TypeOperator:
301
+ if (!ts.isTypeOperatorNode(node)) {
302
+ throw Error("Impossible");
303
+ }
233
304
  if (node.operator === KeyOfKeyword) {
234
305
  const _argument = toSourceTS(node.type);
235
306
  return {
@@ -240,6 +311,9 @@ function toSourceTS(node) {
240
311
  console.warn("unimplemented TypeOperator", node);
241
312
  case TypeReference:
242
313
  {
314
+ if (!ts.isTypeReferenceNode(node)) {
315
+ throw Error("Impossible");
316
+ }
243
317
  if ((typeName.text === 'Object' || typeName.text === 'Record') && (typeArguments == null ? void 0 : typeArguments.length) === 2) {
244
318
  return {
245
319
  type: 'record',
@@ -295,14 +369,16 @@ function toSourceTS(node) {
295
369
  args
296
370
  };
297
371
  }
298
- case StringKeyword:
299
- return node.getText();
300
- case NumberKeyword:
301
- return node.getText();
302
372
  case NamedTupleMember:
373
+ if (!ts.isNamedTupleMember(node)) {
374
+ throw Error("Impossible");
375
+ }
303
376
  return toSourceTS(node.type);
304
377
  case IntersectionType:
305
378
  {
379
+ if (!ts.isIntersectionTypeNode(node)) {
380
+ throw Error("Impossible");
381
+ }
306
382
  const _members = node.types.map(toSourceTS);
307
383
  return {
308
384
  type: 'intersection',
@@ -310,35 +386,87 @@ function toSourceTS(node) {
310
386
  };
311
387
  }
312
388
  case TupleType:
389
+ if (!ts.isTupleTypeNode(node)) {
390
+ throw Error("Impossible");
391
+ }
313
392
  const elements = node.elements.map(toSourceTS);
314
393
  return {
315
394
  type: 'tuple',
316
395
  elements
317
396
  };
318
397
  case UnionType:
398
+ if (!ts.isUnionTypeNode(node)) {
399
+ throw Error("Impossible");
400
+ }
319
401
  const members = node.types.map(toSourceTS);
320
402
  return {
321
403
  type: 'union',
322
404
  members
323
405
  };
324
406
  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
- };
407
+ {
408
+ if (!ts.isTypeLiteralNode(node)) {
409
+ throw Error("Impossible");
410
+ }
411
+ const properties = {};
412
+ /** @type {object[]} */
413
+ let indexSignatures;
414
+ node.members.forEach(member => {
415
+ if (member.kind === IndexSignature) {
416
+ var _indexSignatures;
417
+ indexSignatures = (_indexSignatures = indexSignatures) != null ? _indexSignatures : [];
418
+ indexSignatures.push(toSourceTS(member));
419
+ } else if (member.kind === PropertySignature) {
420
+ if (!ts.isPropertySignature(member)) {
421
+ throw Error("Impossible");
422
+ }
423
+ const name = toSourceTS(member.name);
424
+ const type = toSourceTS(member.type);
425
+ properties[name] = type;
426
+ } else {
427
+ console.warn('TypeLiteral: unhandled member', member);
428
+ }
429
+ });
430
+ const _ret2 = {
431
+ type: 'object'
432
+ };
433
+ if (Object.keys(properties).length) {
434
+ _ret2.properties = properties;
435
+ }
436
+ if (indexSignatures) {
437
+ _ret2.indexSignatures = indexSignatures;
438
+ }
439
+ return _ret2;
440
+ }
335
441
  case PropertySignature:
442
+ if (!ts.isPropertySignature(node)) {
443
+ throw Error("Impossible");
444
+ }
336
445
  console.warn('toSourceTS> should not happen, handled by TypeLiteral directly');
337
446
  return `${toSourceTS(node.name)}: ${toSourceTS(node.type)}`;
447
+ case IndexSignature:
448
+ if (!ts.isIndexSignatureDeclaration(node)) {
449
+ throw Error("Impossible");
450
+ }
451
+ // Only possible modifier I know of, but we don't need it:
452
+ // {readonly [n: number]: string, length: number}
453
+ const indexType = toSourceTS(node.type);
454
+ const indexParameters = node.parameters.map(toSourceTS);
455
+ return {
456
+ type: 'indexSignature',
457
+ indexType,
458
+ indexParameters
459
+ };
338
460
  case Identifier:
461
+ if (!ts.isIdentifier(node)) {
462
+ throw Error("Impossible");
463
+ }
339
464
  return node.text;
340
465
  case ArrayType:
341
466
  {
467
+ if (!ts.isArrayTypeNode(node)) {
468
+ throw Error("Impossible");
469
+ }
342
470
  const elementType = toSourceTS(node.elementType);
343
471
  return {
344
472
  type: 'array',
@@ -346,18 +474,23 @@ function toSourceTS(node) {
346
474
  };
347
475
  }
348
476
  case LiteralType:
477
+ if (!ts.isLiteralTypeNode(node)) {
478
+ throw Error("Impossible");
479
+ }
349
480
  return toSourceTS(node.literal);
350
481
  case AnyKeyword:
351
482
  case BooleanKeyword:
352
- // ts.SyntaxKind[parseType("*").kind] === 'JSDocAllType'
353
- case JSDocAllType:
483
+ case StringKeyword:
484
+ case NeverKeyword:
354
485
  case NullKeyword:
355
- case StringLiteral:
356
- case ThisType:
486
+ case NumberKeyword:
357
487
  case UndefinedKeyword:
358
- case VoidKeyword:
359
488
  case UnknownKeyword:
360
- case NeverKeyword:
489
+ case VoidKeyword:
490
+ // ts.SyntaxKind[parseType("*").kind] === 'JSDocAllType'
491
+ case JSDocAllType:
492
+ case ThisType:
493
+ case StringLiteral:
361
494
  return node.getText();
362
495
  case TrueKeyword:
363
496
  return true;
@@ -371,9 +504,16 @@ function toSourceTS(node) {
371
504
  properties: {}
372
505
  };
373
506
  case ParenthesizedType:
507
+ if (!ts.isParenthesizedTypeNode(node)) {
508
+ throw Error("Impossible");
509
+ }
374
510
  // fall-through for parentheses
375
511
  return toSourceTS(node.type);
376
- case LastTypeNode:
512
+ case ImportType:
513
+ if (!ts.isImportTypeNode(node)) {
514
+ throw Error("Impossible");
515
+ }
516
+ /** @todo Handle case without any qualifier like `import('test')` */
377
517
  return toSourceTS(node.qualifier);
378
518
  default:
379
519
  // const test = {};
@@ -588,18 +728,25 @@ function simplifyType(type, optional) {
588
728
  return type;
589
729
  }
590
730
 
731
+ /**
732
+ * @typedef {ReturnType<typeof parseJSDoc>} ParseJSDocReturnType
733
+ */
734
+ /**
735
+ * @typedef {typeof expandTypeDepFree} ExpandType
736
+ * @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
737
+ */
591
738
  /**
592
739
  * Parses JSDoc comments to extract parameter type information.
593
740
  *
594
741
  * @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.
742
+ * @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
743
+ * @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
597
744
  */
598
745
  function parseJSDoc(src, expandType = expandTypeDepFree) {
599
746
  // Parse something like: @param {Object} [kwargs={}] Optional arguments.
600
747
  const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\{\}\.'" ]+)/g;
601
748
  const matches = [...src.matchAll(regex)];
602
- /** @type {Record<string, any>} */
749
+ /** @type {Record<string, ExpandTypeReturnType>} */
603
750
  const params = Object.create(null);
604
751
  matches.forEach(_ => {
605
752
  const type = expandType(_[1].trim());
@@ -619,44 +766,31 @@ function parseJSDoc(src, expandType = expandTypeDepFree) {
619
766
  // Strip the rest (either leftover of optional value or description)
620
767
  name = name.split(' ')[0].split('=')[0].trim();
621
768
  const simplifiedType = simplifyType(type, optional);
622
- const parts = name.split(".");
623
- if (parts.length === 3) {
624
- // Something like: @param {number[]} settings.render.skyboxRotation - Rotation of skybox.
625
- const parts0 = parts[0]; // settings
626
- const parts1 = parts[1]; // render
627
- const parts2 = parts[2]; // skyboxRotation
628
- const toptype = params[parts0];
629
- toptype.properties[parts1].properties = toptype.properties[parts1].properties || {};
630
- toptype.properties[parts1].properties[parts2] = simplifiedType;
631
- } else if (parts.length === 2) {
632
- // Something like: @param {number} description[].components
633
- let parts0 = parts[0]; // description[]
634
- const parts1 = parts[1]; // components
635
- if (parts0.endsWith('[]')) {
636
- parts0 = parts0.slice(0, -2); // description[] -> description
637
- }
638
-
639
- const toptype = params[parts0];
640
- if ((toptype == null ? void 0 : toptype.type) === "union") {
769
+ // Turn "options.stats[].unitsName" into ['options', 'stats', 'unitsName'].
770
+ const parts = name.split(/[\[\]]*\./);
771
+ let properties = params;
772
+ for (const part of parts) {
773
+ const toptype = properties[part];
774
+ if (!toptype) {
775
+ // No toptype means we resolved as far as possible, now we can add `simplifiedType`.
776
+ console.assert(part === parts.at(-1), 'Current part and last part should be the same.');
777
+ properties[part] = simplifiedType;
778
+ } else if (toptype.type === "union") {
641
779
  const typeObject = toptype.members.find(_ => (_ == null ? void 0 : _.type) === 'object');
642
- typeObject.properties[parts1] = simplifiedType;
643
- //typeObject.properties = simplifiedType; // todo add test case
644
- } else if ((toptype == null ? void 0 : toptype.type) === "array") {
645
- toptype.elementType.properties[parts1] = simplifiedType;
646
- } else if ((toptype == null ? void 0 : toptype.type) === "object") {
647
- toptype.properties = toptype.properties || {};
648
- toptype.properties[parts1] = simplifiedType;
780
+ properties = typeObject.properties;
781
+ } else if (toptype.type === "array") {
782
+ properties = toptype.elementType.properties;
783
+ } else if (toptype.type === "object") {
784
+ toptype.properties = toptype.properties || Object.create(null);
785
+ properties = toptype.properties;
649
786
  } else {
650
- console.warn("parseJSDoc> skipping @param, unseen syntax detected, please check if your JSDoc is valid or open an issue about this", {
787
+ console.warn("parseJSDoc> Skipping @param, unseen syntax detected. Please check if your JSDoc is valid or open an issue about this!", {
651
788
  src,
652
789
  toptype,
653
- parts0,
654
- parts1,
790
+ parts,
655
791
  simplifiedType
656
792
  });
657
793
  }
658
- } else {
659
- params[name] = simplifiedType;
660
794
  }
661
795
  });
662
796
  if (Object.keys(params).length === 0) {
@@ -682,6 +816,34 @@ function parseJSDocSetter(src, expandType = expandTypeDepFree) {
682
816
  }
683
817
  }
684
818
 
819
+ /**
820
+ * @typedef {typeof expandTypeDepFree} ExpandType
821
+ * @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
822
+ */
823
+ /**
824
+ * Parses JSDoc comments to extract parameter type information.
825
+ *
826
+ * @param {string} src - The JSDoc comment string to parse.
827
+ * @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
828
+ * @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping template names to their parsed types,
829
+ * or `undefined` if no template tags were found.
830
+ */
831
+ function parseJSDocTemplates(src, expandType = expandTypeDepFree) {
832
+ const regexTemplateTyped = /@template \{(.*?)\} ([a-zA-Z0-9_$=]+)/g;
833
+ const matches = [...src.matchAll(regexTemplateTyped)];
834
+ if (!matches.length) {
835
+ return;
836
+ }
837
+ /** @type {Record<string, ExpandTypeReturnType>} */
838
+ const templates = Object.create(null);
839
+ matches.forEach(_ => {
840
+ const type = expandType(_[1].trim());
841
+ const name = _[2].trim();
842
+ templates[name] = type;
843
+ });
844
+ return templates;
845
+ }
846
+
685
847
  /**
686
848
  * Extracts the parameter name and its optionality from a JSDoc parameter string.
687
849
  *
@@ -2631,14 +2793,14 @@ class Stringifier {
2631
2793
  }
2632
2794
 
2633
2795
  /** @typedef {import('@babel/types').Node } Node */
2634
- /** @typedef {import("@babel/types").ClassMethod } ClassMethod */
2635
- /** @typedef {import("@babel/types").ClassPrivateMethod} ClassPrivateMethod */
2636
- /** @typedef {import('./stat.js').Stat } Stat */
2796
+ /** @typedef {import('@babel/types').ClassMethod } ClassMethod */
2797
+ /** @typedef {import('@babel/types').ClassPrivateMethod} ClassPrivateMethod */
2798
+ /** @typedef {import('./stat.js').Stat } Stat */
2637
2799
  /**
2638
2800
  * @typedef {object} Options
2639
2801
  * @property {boolean} [forceCurly] - Determines whether curly braces are enforced in Stringifier.
2640
2802
  * @property {boolean} [validateDivision] - Indicates whether division operations should be validated.
2641
- * @property {Function} [expandType] - A function that expands shorthand types into full descriptions.
2803
+ * @property {import('./parseJSDoc.js').ExpandType} [expandType] - A function that expands shorthand types into full descriptions.
2642
2804
  * @property {string} [filename] - The name of a file to which the instance pertains.
2643
2805
  * @property {boolean} [addHeader] - Whether to add import declarations headers. Defaults to true.
2644
2806
  * @property {string[]} [ignoreLocations] - Ignore these locations because they are known false-positives.
@@ -2891,9 +3053,9 @@ class Asserter extends Stringifier {
2891
3053
  }
2892
3054
  /**
2893
3055
  * @param {Node} node - The Babel AST node.
2894
- * @returns {undefined | {}} The return value of `parseJSDoc`.
3056
+ * @returns {string|undefined} The JSDoc comment of `node`.
2895
3057
  */
2896
- getJSDoc(node) {
3058
+ getLeadingComment(node) {
2897
3059
  if (node.type === 'BlockStatement') {
2898
3060
  node = this.parent;
2899
3061
  }
@@ -2919,28 +3081,56 @@ class Asserter extends Stringifier {
2919
3081
  if (leadingComments && leadingComments.length) {
2920
3082
  const lastComment = leadingComments[leadingComments.length - 1];
2921
3083
  if (lastComment.type === "CommentBlock") {
2922
- if (lastComment.value.includes('@event')) {
2923
- return;
2924
- }
2925
- if (node.type === 'ClassMethod' && node.kind === 'set') {
2926
- const paramName = this.getNameOfParam(node.params[0]);
2927
- if (node.params.length !== 1) {
2928
- this.warn("getJSDoc> setters require exactly one argument");
2929
- }
2930
- const setterType = parseJSDocSetter(lastComment.value, this.expandType);
2931
- if (!setterType) {
2932
- return;
2933
- }
2934
- return {
2935
- [paramName]: setterType
2936
- };
2937
- }
2938
- if (lastComment.value.includes('@ignoreRTI')) {
2939
- return;
2940
- }
2941
- return parseJSDoc(lastComment.value, this.expandType);
3084
+ return lastComment.value;
3085
+ }
3086
+ }
3087
+ }
3088
+ /**
3089
+ * @param {Node} node - The Babel AST node.
3090
+ * @todo ESLint problem:
3091
+ * returns {import('./parseJSDoc.js').ParseJSDocReturnType} The return value of `parseJSDoc`
3092
+ * returns {Record<string, import('./parseJSDoc.js').ExpandTypeReturnType> | undefined} The
3093
+ * return value of `parseJSDoc`.
3094
+ * @returns {Record<string, any>|undefined} asd
3095
+ */
3096
+ getJSDoc(node) {
3097
+ const comment = this.getLeadingComment(node);
3098
+ if (!comment) {
3099
+ return;
3100
+ }
3101
+ if (comment.includes('@event')) {
3102
+ return;
3103
+ }
3104
+ if (comment.includes('@ignoreRTI')) {
3105
+ return;
3106
+ }
3107
+ // Need to do same resolving as in: this.getLeadingComment(node)
3108
+ if (node.type === 'BlockStatement') {
3109
+ node = this.parent;
3110
+ }
3111
+ if (node.type === 'ClassMethod' && node.kind === 'set') {
3112
+ const paramName = this.getNameOfParam(node.params[0]);
3113
+ if (node.params.length !== 1) {
3114
+ this.warn("getJSDoc> setters require exactly one argument");
2942
3115
  }
3116
+ const setterType = parseJSDocSetter(comment, this.expandType);
3117
+ if (!setterType) {
3118
+ return;
3119
+ }
3120
+ const _params = {
3121
+ [paramName]: setterType
3122
+ };
3123
+ return {
3124
+ templates: undefined,
3125
+ params: _params
3126
+ };
2943
3127
  }
3128
+ const templates = parseJSDocTemplates(comment);
3129
+ const params = parseJSDoc(comment, this.expandType);
3130
+ return {
3131
+ templates,
3132
+ params
3133
+ };
2944
3134
  }
2945
3135
  /**
2946
3136
  * Retrieves the name of a parameter from a Babel AST node.
@@ -2959,8 +3149,7 @@ class Asserter extends Stringifier {
2959
3149
  return param.left.name;
2960
3150
  }
2961
3151
  }
2962
- debugger;
2963
- this.warn("unable to extra name from param in specified way - may contain too much information");
3152
+ this.warn("Unable to retrieve name from param in specified way - may contain too much information.");
2964
3153
  return this.toSource(param);
2965
3154
  }
2966
3155
  statsReset() {
@@ -3079,6 +3268,17 @@ class Asserter extends Stringifier {
3079
3268
  stat.unchecked++;
3080
3269
  return '';
3081
3270
  }
3271
+ const {
3272
+ templates,
3273
+ params
3274
+ } = jsdoc;
3275
+ if (!params) {
3276
+ console.warn("This should never happen, please check your input code.", this.getLeadingComment(node), {
3277
+ jsdoc
3278
+ });
3279
+ stat.unchecked++;
3280
+ return '';
3281
+ }
3082
3282
  stat.checked++;
3083
3283
  const {
3084
3284
  spaces
@@ -3089,17 +3289,21 @@ class Asserter extends Stringifier {
3089
3289
  if (this.ignoreLocations.includes(loc)) {
3090
3290
  return '// IGNORE RTI TYPE VALIDATIONS, KNOWN ISSUES\n';
3091
3291
  }
3292
+ if (templates) {
3293
+ const tmp = JSON.stringify(templates, null, 2).replaceAll('\n', '\n' + spaces);
3294
+ out += `\n${spaces}const rtiTemplates = ${tmp};`;
3295
+ }
3092
3296
  //out += `${spaces}/*${spaces} node.type=${node.type}\n${spaces}
3093
3297
  // ${JSON.stringify(jsdoc)}\n${parent}\n${spaces}*/\n`;
3094
- for (let name in jsdoc) {
3095
- const type = jsdoc[name];
3298
+ for (let name in params) {
3299
+ const type = params[name];
3096
3300
  const hasParam = this.nodeHasParamName(node, name);
3097
3301
  if (!hasParam) {
3098
3302
  let testNode = node;
3099
3303
  if (node.type === 'BlockStatement') {
3100
3304
  testNode = this.parent;
3101
3305
  }
3102
- const paramIndex = Object.keys(jsdoc).findIndex(_ => _ === name);
3306
+ const paramIndex = Object.keys(params).findIndex(_ => _ === name);
3103
3307
  const param = testNode.params[paramIndex];
3104
3308
  if (param) {
3105
3309
  const isObjectPattern = param.type === 'ObjectPattern';
@@ -3129,7 +3333,11 @@ class Asserter extends Stringifier {
3129
3333
  continue;
3130
3334
  }
3131
3335
  const _t = JSON.stringify(type.elementType, null, 2).replaceAll('\n', '\n' + spaces);
3132
- out += `${spaces}if (!inspectType(${element.name}, ${_t}, '${_loc}', '${name}')) {\n`;
3336
+ if (templates) {
3337
+ out += `${spaces}if (!inspectTypeWithTemplates(${element.name}, ${_t}, '${_loc}', '${name}', rtiTemplates)) {\n`;
3338
+ } else {
3339
+ out += `${spaces}if (!inspectType(${element.name}, ${_t}, '${_loc}', '${name}')) {\n`;
3340
+ }
3133
3341
  out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3134
3342
  }
3135
3343
  continue;
@@ -3151,7 +3359,11 @@ class Asserter extends Stringifier {
3151
3359
  continue;
3152
3360
  }
3153
3361
  const _t2 = JSON.stringify(subType, null, 2).replaceAll('\n', '\n' + spaces);
3154
- out += `${spaces}if (!inspectType(${keyName}, ${_t2}, '${_loc}', '${name}')) {\n`;
3362
+ if (templates) {
3363
+ out += `${spaces}if (!inspectTypeWithTemplates(${keyName}, ${_t2}, '${_loc}', '${name}', rtiTemplates)) {\n`;
3364
+ } else {
3365
+ out += `${spaces}if (!inspectType(${keyName}, ${_t2}, '${_loc}', '${name}')) {\n`;
3366
+ }
3155
3367
  out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3156
3368
  }
3157
3369
  continue;
@@ -3184,7 +3396,11 @@ class Asserter extends Stringifier {
3184
3396
  out += '\n';
3185
3397
  first = false;
3186
3398
  }
3187
- out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${_loc3}', '${name}')) {\n`;
3399
+ if (templates) {
3400
+ out += `${spaces}if (${prevCheck}!inspectTypeWithTemplates(${name}, ${t}, '${_loc3}', '${name}', rtiTemplates)) {\n`;
3401
+ } else {
3402
+ out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${_loc3}', '${name}')) {\n`;
3403
+ }
3188
3404
  out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3189
3405
  }
3190
3406
  return out;
@@ -3710,4 +3926,4 @@ function toSourceBabelTS(node) {
3710
3926
  }
3711
3927
  }
3712
3928
 
3713
- export { Asserter, Stringifier, addTypeChecks, ast2json, ast2jsonForComparison, code2ast2code, compareAST, expandType, expandTypeBabelTS, expandTypeDepFree, extractCurlyContent, extractNameAndOptionality, nodeIsFunction, parseJSDoc, parseJSDocSetter, parseJSDocTypedef, parseType, parseTypeBabelTS, requiredTypeofs, simplifyType, toSourceBabelTS, toSourceTS, trimEndSpaces };
3929
+ export { Asserter, Stringifier, addTypeChecks, ast2json, ast2jsonForComparison, code2ast2code, compareAST, expandType, expandTypeBabelTS, expandTypeDepFree, extractCurlyContent, extractNameAndOptionality, nodeIsFunction, parseJSDoc, parseJSDocSetter, parseJSDocTemplates, parseJSDocTypedef, parseType, parseTypeBabelTS, requiredTypeofs, simplifyType, toSourceBabelTS, toSourceTS, trimEndSpaces };