bio-dts 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,7 +86,7 @@ export function parse(str) {
86
86
  i = skipWhitespace(str, j);
87
87
 
88
88
  // parse type
89
- if ([ 'param', 'return', 'returns', 'typedef' ].includes(tag.name)) {
89
+ if ([ 'param', 'return', 'returns', 'typedef', 'template' ].includes(tag.name)) {
90
90
 
91
91
  let match = findClose(str, i, '{', '}');
92
92
 
@@ -98,7 +98,7 @@ export function parse(str) {
98
98
  }
99
99
 
100
100
  // parse param
101
- if ([ 'param', 'typedef' ].includes(tag.name)) {
101
+ if ([ 'param', 'typedef', 'template' ].includes(tag.name)) {
102
102
 
103
103
  let match = tag.name === 'param' && findClose(str, i, '[', ']');
104
104
 
@@ -82,6 +82,56 @@ export default function transform(src) {
82
82
  }));
83
83
  }
84
84
 
85
+ function getDocumentedParams(node, params = [], comment = null) {
86
+
87
+ if (!comment) {
88
+ return params;
89
+ }
90
+
91
+ const doc = comment.value.replace(/\n\s+/g, '\n ');
92
+
93
+ // parse known parameters
94
+ // ignore hierarchical (sub-type) params
95
+ const knownParams =
96
+ parseJSDoc(doc)
97
+ .filter(tag => tag.name === 'param' && !tag.param.name.includes('.'))
98
+ .map(tag => tag.param.name);
99
+
100
+ if (!knownParams.length) {
101
+ return params;
102
+ }
103
+
104
+ let i = 0;
105
+ let j = 0;
106
+
107
+ const filteredParams = [];
108
+
109
+ while (j < knownParams.length) {
110
+
111
+ let expectedName = knownParams[j];
112
+ let param = params[i++];
113
+
114
+ if (!param) {
115
+ throw error(node, `documented parameter <${ expectedName }> not found`);
116
+ }
117
+
118
+ if (param.name === 'this') {
119
+ filteredParams.push(param);
120
+ continue;
121
+ }
122
+
123
+ const actualName = param.argument?.name || param.name;
124
+
125
+ if (actualName !== expectedName) {
126
+ throw error(node, `documented parameter <${ expectedName }> differs from actual parameter <${ actualName }>`);
127
+ }
128
+
129
+ filteredParams.push(param);
130
+ j++;
131
+ }
132
+
133
+ return filteredParams;
134
+ }
85
135
 
86
136
  /**
87
137
  * Ensure optional args methods are properly escaped
@@ -96,40 +146,34 @@ export default function transform(src) {
96
146
  value: node
97
147
  } = nodePath;
98
148
 
99
- const fnDeclaration = (
100
- node.type === 'TSDeclareMethod' ||
101
- node.type === 'TSDeclareFunction'
102
- );
149
+ const functionKind = getFunctionKind(node);
103
150
 
104
- const fnDefinition = (
105
- node?.typeAnnotation?.typeAnnotation?.type === 'TSFunctionType'
106
- );
107
-
108
- if (!fnDeclaration && !fnDefinition) {
151
+ if (!functionKind) {
109
152
  return;
110
153
  }
111
154
 
112
- const params = fnDeclaration
113
- ? node.params
114
- : node?.typeAnnotation?.typeAnnotation?.parameters;
155
+ const params = functionKind === 'TSFunctionType'
156
+ ? node.typeAnnotation?.typeAnnotation?.parameters
157
+ : node.params;
158
+
159
+ const typeParameters = functionKind === 'TSFunctionType'
160
+ ? node.typeAnnotation?.typeAnnotation?.typeParameters
161
+ : node.typeParameters;
115
162
 
116
- const typeParameters = fnDeclaration
117
- ? node.typeParameters
118
- : node?.typeAnnotation?.typeAnnotation?.typeParameters;
163
+ const returnType = functionKind === 'TSFunctionType'
164
+ ? node.typeAnnotation?.typeAnnotation?.typeAnnotation
165
+ : node.returnType;
119
166
 
120
- const returnType = fnDeclaration
121
- ? node.returnType
122
- : node?.typeAnnotation?.typeAnnotation?.typeAnnotation;
167
+ const hostPath = getHostPath(nodePath);
123
168
 
124
- const hostPath = (
125
- [ 'ExportDefaultDeclaration', 'ExportNamedDeclaration' ].includes(nodePath.parentPath.value.type)
126
- ? nodePath.parentPath
127
- : nodePath
128
- );
169
+ const commentPaths = hostPath.get('comments');
170
+
171
+ // last comment is significant
172
+ const commentPath = commentPaths?.value && commentPaths.get(commentPaths.value.length - 1) || { value: null };
129
173
 
130
174
  const {
131
175
  variations
132
- } = params.slice().reverse().reduce((res, param) => {
176
+ } = getDocumentedParams(hostPath.value, params, commentPath.value).slice().reverse().reduce((res, param) => {
133
177
 
134
178
  let {
135
179
  required,
@@ -183,11 +227,6 @@ export default function transform(src) {
183
227
  return;
184
228
  }
185
229
 
186
- const commentPaths = hostPath.get('comments');
187
-
188
- // last comment is significant
189
- const commentPath = commentPaths?.value && commentPaths.get(commentPaths.value.length - 1) || { value: null };
190
-
191
230
  const replacements = variations.slice().reverse().map(variation => {
192
231
 
193
232
  const builder = {
@@ -267,6 +306,182 @@ export default function transform(src) {
267
306
  replace(nodePath, nodePath.get('declaration').value);
268
307
  }
269
308
 
309
+ /**
310
+ * Ensure that only documented method parameters are used.
311
+ *
312
+ * @param {Path} nodePath
313
+ *
314
+ * @return {boolean} true if modified
315
+ */
316
+ function removeUnknownParams(nodePath) {
317
+
318
+ const {
319
+ value: node
320
+ } = nodePath;
321
+
322
+ const functionKind = getFunctionKind(node);
323
+
324
+ if (!functionKind) {
325
+ return;
326
+ }
327
+
328
+ const params = functionKind === 'TSFunctionType'
329
+ ? nodePath.get('typeAnnotation', 'typeAnnotation', 'parameters')
330
+ : nodePath.get('params');
331
+
332
+ const hostPath = getHostPath(nodePath);
333
+
334
+ const commentPaths = hostPath.get('comments');
335
+
336
+ // last comment is significant
337
+ const commentPath = commentPaths?.value && commentPaths.get(commentPaths.value.length - 1) || { value: null };
338
+
339
+ const knownParams = getDocumentedParams(hostPath.value, params.value, commentPath.value);
340
+
341
+ let replaced = false;
342
+
343
+ for (const idx in params.value || []) {
344
+ if (!knownParams[idx]) {
345
+ replace(params.get(idx));
346
+ replaced = true;
347
+ }
348
+ }
349
+
350
+ return replaced;
351
+ }
352
+
353
+ /**
354
+ * Generate method overloads based on `@overlord` annotated JSDoc comments.
355
+ *
356
+ * Our strategy is to parse for separate `@overlord` annotated tags,
357
+ * use the meta-data, and generate a completely new method from it.
358
+ *
359
+ * @param {Path} nodePath
360
+ *
361
+ * @return {boolean} true if modified
362
+ */
363
+ function generateOverloads(nodePath) {
364
+
365
+ const {
366
+ value: node
367
+ } = nodePath;
368
+
369
+ const functionKind = getFunctionKind(node);
370
+
371
+ if (!functionKind) {
372
+ return;
373
+ }
374
+
375
+ const hostPath = getHostPath(nodePath);
376
+
377
+ const commentPaths = hostPath.get('comments');
378
+
379
+ const comments = (commentPaths?.value || []);
380
+
381
+ // scan for potential overloads
382
+ const overloads = comments.reduce((overloads, comment, idx) => {
383
+ const commentText = comment.value;
384
+
385
+ if (commentText.includes('@overlord') || idx === comments.length - 1) {
386
+ const tags = parseJSDoc(comment.value);
387
+
388
+ const paramTags = tags.filter(t => t.name === 'param');
389
+ const templateTags = tags.filter(t => t.name === 'template');
390
+ const returnTags = tags.filter(t => t.name === 'return' || t.name === 'returns');
391
+
392
+ if (returnTags.length > 1) {
393
+ throw error(hostPath.value, 'must specify zero or one @return(s) type in @overload fn');
394
+ }
395
+
396
+ return [
397
+ ...overloads,
398
+ {
399
+ comment,
400
+ templateTags,
401
+ paramTags,
402
+ returnTag: returnTags[0]
403
+ }
404
+ ];
405
+ }
406
+
407
+ return overloads;
408
+ }, []);
409
+
410
+ if (overloads.length < 2) {
411
+ return;
412
+ }
413
+
414
+ const replacements = overloads.slice().reverse().map(overload => {
415
+
416
+ const {
417
+ comment,
418
+ templateTags,
419
+ paramTags,
420
+ returnTag
421
+ } = overload;
422
+
423
+ const methodCode = `/*${cleanComment(comment).value}*/
424
+ declare function p${
425
+ templateTags.length ? '<' + templateTags.map(
426
+ t => [
427
+ t.param.name,
428
+ t.type ? 'extends ' + t.type.value.slice(1, -1) : null
429
+ ].filter(f => f).join(' ')
430
+ ).join(',') + '>' : ''
431
+ }(${
432
+ paramTags.map(p => {
433
+
434
+ return [
435
+ p.param.name,
436
+ p.param.value.startsWith('[') ? '?' : null,
437
+ ': ',
438
+ p.type.value.slice(1, -1)
439
+ ].filter(f => f).join('');
440
+ }).join(',')
441
+ }) : ${ returnTag ? returnTag.type.value.slice(1, -1) : 'void' };`;
442
+
443
+ const func = parseDts(methodCode).program.body[0];
444
+
445
+ const builder = {
446
+ 'ClassProperty': b.tsDeclareMethod,
447
+ 'TSDeclareFunction': b.tsDeclareFunction,
448
+ 'TSDeclareMethod': b.tsDeclareMethod
449
+ }[node.type];
450
+
451
+ const hostBuilder = {
452
+ 'ExportNamedDeclaration': b.exportNamedDeclaration,
453
+ 'ExportDefaultDeclaration': b.exportDefaultDeclaration
454
+ }[hostPath.value.type];
455
+
456
+ const newNode = builder.from({
457
+ key: node.key,
458
+ params: func.params,
459
+ id: node.id,
460
+ declare: node.declare || false,
461
+ returnType: func.returnType ? b.tsTypeAnnotation.from({
462
+ ...func.returnType
463
+ }) : null,
464
+ comments: hostPath === nodePath ? func.comments : [],
465
+ typeParameters: func.typeParameters ? b.tsTypeParameterDeclaration.from({
466
+ ...func.typeParameters
467
+ }) : null
468
+ });
469
+
470
+ return (
471
+ hostBuilder
472
+ ? hostBuilder.from({
473
+ ...hostPath.value,
474
+ declaration: newNode,
475
+ comments: func.comments
476
+ })
477
+ : newNode
478
+ );
479
+ });
480
+
481
+ replace(hostPath, ...replacements);
482
+
483
+ return replacements;
484
+ }
270
485
 
271
486
  function cleanComment(comment) {
272
487
 
@@ -278,7 +493,10 @@ export default function transform(src) {
278
493
  for (const tag of tags) {
279
494
 
280
495
  // remove full line including the non-TS tag
281
- if (/class|constructor|template|method|typedef|property/.test(tag.name)) {
496
+ if (
497
+ /class|constructor|template|method|typedef|property|this|overlord/.test(tag.name) ||
498
+ tag.param?.name?.includes('.')
499
+ ) {
282
500
  replacements.push([ { start: tag.start - 4, end: tag.end } ]);
283
501
 
284
502
  continue;
@@ -345,24 +563,27 @@ export default function transform(src) {
345
563
  return replace(path);
346
564
  }
347
565
 
348
- if (fixOptionalArgsMethods(path)) {
566
+ if (generateOverloads(path)) {
349
567
  return false;
350
568
  }
351
569
 
352
- if (fixTypeExport(path)) {
570
+ if (fixOptionalArgsMethods(path)) {
353
571
  return false;
354
572
  }
355
573
 
574
+ removeUnknownParams(path);
356
575
  cleanComments(path);
576
+
577
+ fixTypeExport(path);
357
578
  });
358
579
 
359
580
  for (const [ path, args ] of replacements) {
360
581
  try {
361
582
  path.replace(...args);
362
583
  } catch (err) {
363
- console.error('Failed to replace path', path, ...args);
584
+ console.error('Failed to replace path', path, ...args, err);
364
585
 
365
- throw err;
586
+ throw error(path.value, 'failed to replace path: ' + err.message);
366
587
  }
367
588
  }
368
589
 
@@ -387,4 +608,61 @@ function traverse(path, cb) {
387
608
 
388
609
  traverse(path.get(key), cb);
389
610
  }
611
+ }
612
+
613
+
614
+ /**
615
+ * @param {any} node
616
+ * @return {'TSDeclareMethod' | 'TSDeclareFunction' | 'TSFunctionType' | null}
617
+ */
618
+ function getFunctionKind(node) {
619
+
620
+ if (
621
+ node.type === 'TSDeclareMethod'
622
+ ) {
623
+ return 'TSDeclareMethod';
624
+ }
625
+
626
+ if (node.type === 'TSDeclareFunction') {
627
+ return 'TSDeclareFunction';
628
+ }
629
+
630
+ if (
631
+ node.typeAnnotation?.typeAnnotation?.type === 'TSFunctionType'
632
+ ) {
633
+ return 'TSFunctionType';
634
+ }
635
+
636
+ return null;
637
+ }
638
+
639
+ /**
640
+ * Return host path for node (with attached comments).
641
+ *
642
+ * @param {Path} nodePath
643
+ *
644
+ * @return {Path}
645
+ */
646
+ function getHostPath(nodePath) {
647
+ return [ 'ExportDefaultDeclaration', 'ExportNamedDeclaration' ].includes(nodePath.parentPath.value.type)
648
+ ? nodePath.parentPath
649
+ : nodePath;
650
+ }
651
+
652
+ /**
653
+ * @param {Node} node
654
+ *
655
+ * @return {string}
656
+ */
657
+ function error(node, message) {
658
+
659
+ const {
660
+ loc: {
661
+ start
662
+ }
663
+ } = node;
664
+
665
+ const loc = `[line ${start.line + 1}, column ${start.column + 1}]`;
666
+
667
+ return new Error(`${message} ${loc}`);
390
668
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bio-dts",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Generate sane and clean types from JavaScript sources",
5
5
  "type": "module",
6
6
  "main": "index.js",