@runtime-type-inspector/transpiler 4.0.6 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/index.cjs +4467 -235
- package/index.mjs +4093 -223
- package/package.json +6 -3
- package/transpiler.js +9 -0
- package/ts2js.js +9 -0
- package/types.d.ts +970 -0
- package/bin.js +0 -11
package/index.mjs
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
import { parse } from '@babel/parser';
|
|
2
2
|
import ts from 'typescript';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @typedef DocType
|
|
6
|
+
* @property {boolean} optional - Type is optional.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Annotates a type with optionality for internal use (parseJSDoc navigation).
|
|
10
|
+
* Sets optionality on object types and wraps bare strings that need to be
|
|
11
|
+
* expandable containers (e.g. 'object', 'object[]', 'union') into objects
|
|
12
|
+
* so that parseJSDoc can later navigate into their properties.
|
|
13
|
+
* Does NOT recurse into compound types or delete properties — those are
|
|
14
|
+
* kept fully expanded so that parseJSDoc and JSDocAnnotator can still
|
|
15
|
+
* mutate them (e.g. appending nested @param lines).
|
|
16
|
+
* Literal types (numbers/booleans) pass through bare unless optional,
|
|
17
|
+
* mirroring the string path.
|
|
18
|
+
* @param {string | number | boolean | DocType} type - The type.
|
|
19
|
+
* @param {boolean} optional - Optionality
|
|
20
|
+
* @returns {string | number | boolean | DocType} The annotated type.
|
|
21
|
+
*/
|
|
22
|
+
function annotateOptional(type, optional) {
|
|
23
|
+
// If it's already an object, just set optionality.
|
|
24
|
+
if (type instanceof Object) {
|
|
25
|
+
type.optional = optional;
|
|
26
|
+
} else if (typeof type === 'number' || typeof type === 'boolean') {
|
|
27
|
+
if (!optional) {
|
|
28
|
+
return type;
|
|
29
|
+
}
|
|
30
|
+
type = {
|
|
31
|
+
type,
|
|
32
|
+
optional
|
|
33
|
+
};
|
|
34
|
+
} else if (typeof type === 'string') {
|
|
35
|
+
type = type.trim();
|
|
36
|
+
if (type !== 'object' && type !== 'object[]' && type !== 'union' && !optional) {
|
|
37
|
+
return type;
|
|
38
|
+
}
|
|
39
|
+
type = {
|
|
40
|
+
type,
|
|
41
|
+
optional
|
|
42
|
+
};
|
|
43
|
+
} else {
|
|
44
|
+
debugger;
|
|
45
|
+
console.warn("annotateOptional> neither object nor string for type", type);
|
|
46
|
+
}
|
|
47
|
+
return type;
|
|
48
|
+
}
|
|
49
|
+
|
|
4
50
|
/**
|
|
5
51
|
* Transforms a type string into a structured type representation.
|
|
6
52
|
*
|
|
@@ -157,6 +203,10 @@ function toSourceTS(node) {
|
|
|
157
203
|
// parseType('keyof typeof obj' ).kind === ts.SyntaxKind.TypeOperator
|
|
158
204
|
KeyOfKeyword,
|
|
159
205
|
// parseType('keyof typeof obj' ).operator === ts.SyntaxKind.KeyOfKeyword
|
|
206
|
+
ReadonlyKeyword,
|
|
207
|
+
// parseType('readonly number[]' ).operator === ts.SyntaxKind.ReadonlyKeyword
|
|
208
|
+
UniqueKeyword,
|
|
209
|
+
// parseType('unique symbol' ).operator === ts.SyntaxKind.UniqueKeyword
|
|
160
210
|
ConstructorType,
|
|
161
211
|
// parseType('new (...args: any[]) => any' ).kind === ts.SyntaxKind.ConstructorType
|
|
162
212
|
NamedTupleMember,
|
|
@@ -165,7 +215,11 @@ function toSourceTS(node) {
|
|
|
165
215
|
// parseType('{[K in TaskType]: 123}' ).kind === ts.SyntaxKind.MappedType
|
|
166
216
|
TypeParameter,
|
|
167
217
|
// parseType('{[K in TaskType]: 123}' ).typeParameter.kind === ts.SyntaxKind.TypeParameter
|
|
168
|
-
QualifiedName
|
|
218
|
+
QualifiedName,
|
|
219
|
+
// parseType("import('abc').x.y" ).qualifier.kind === ts.SyntaxKind.QualifiedName
|
|
220
|
+
TemplateLiteralType,
|
|
221
|
+
// parseType('`${A}_id`' ).kind === ts.SyntaxKind.TemplateLiteralType
|
|
222
|
+
NoSubstitutionTemplateLiteral // parseType('`id`' ).literal.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral
|
|
169
223
|
} = ts.SyntaxKind;
|
|
170
224
|
// console.log({typeArguments, typeName, kind_, node});
|
|
171
225
|
switch (node.kind) {
|
|
@@ -328,9 +382,15 @@ function toSourceTS(node) {
|
|
|
328
382
|
argument: _argument
|
|
329
383
|
};
|
|
330
384
|
}
|
|
385
|
+
if (node.operator === ReadonlyKeyword) {
|
|
386
|
+
// readonly erased at runtime, same shape as the inner type.
|
|
387
|
+
return toSourceTS(node.type);
|
|
388
|
+
}
|
|
331
389
|
console.warn("unimplemented TypeOperator", node);
|
|
390
|
+
return 'any';
|
|
332
391
|
case TypeReference:
|
|
333
392
|
{
|
|
393
|
+
var _typeName$text;
|
|
334
394
|
if (!ts.isTypeReferenceNode(node)) {
|
|
335
395
|
throw Error("Impossible");
|
|
336
396
|
}
|
|
@@ -381,7 +441,8 @@ function toSourceTS(node) {
|
|
|
381
441
|
if (!typeArguments) {
|
|
382
442
|
return typeName.getText();
|
|
383
443
|
}
|
|
384
|
-
|
|
444
|
+
// Qualified names (e.g. `some.name.space.Array<T>`) have no `.text`.
|
|
445
|
+
const _name = (_typeName$text = typeName.text) != null ? _typeName$text : typeName.getText();
|
|
385
446
|
const args = typeArguments.map(toSourceTS);
|
|
386
447
|
return {
|
|
387
448
|
type: 'reference',
|
|
@@ -390,10 +451,25 @@ function toSourceTS(node) {
|
|
|
390
451
|
};
|
|
391
452
|
}
|
|
392
453
|
case NamedTupleMember:
|
|
393
|
-
|
|
394
|
-
|
|
454
|
+
{
|
|
455
|
+
if (!ts.isNamedTupleMember(node)) {
|
|
456
|
+
throw Error("Impossible");
|
|
457
|
+
}
|
|
458
|
+
const nName = toSourceTS(node.name);
|
|
459
|
+
const nType = toSourceTS(node.type);
|
|
460
|
+
const opt = !!node.questionToken;
|
|
461
|
+
const dot = !!node.dotDotDotToken;
|
|
462
|
+
// Preserve label for docs: [a: string, b: number] -> keep name
|
|
463
|
+
// Use tupleMember shape so stringify can round-trip
|
|
464
|
+
const mem = {
|
|
465
|
+
type: 'tupleMember',
|
|
466
|
+
name: nName,
|
|
467
|
+
elementType: nType
|
|
468
|
+
};
|
|
469
|
+
if (opt) mem.optional = true;
|
|
470
|
+
if (dot) mem.dotDot = true;
|
|
471
|
+
return mem;
|
|
395
472
|
}
|
|
396
|
-
return toSourceTS(node.type);
|
|
397
473
|
case IntersectionType:
|
|
398
474
|
{
|
|
399
475
|
if (!ts.isIntersectionTypeNode(node)) {
|
|
@@ -414,6 +490,34 @@ function toSourceTS(node) {
|
|
|
414
490
|
type: 'tuple',
|
|
415
491
|
elements
|
|
416
492
|
};
|
|
493
|
+
case TemplateLiteralType:
|
|
494
|
+
{
|
|
495
|
+
if (!ts.isTemplateLiteralTypeNode(node)) {
|
|
496
|
+
throw Error("Impossible");
|
|
497
|
+
}
|
|
498
|
+
// A template literal type is a sequence of literal chunks (quasis) with
|
|
499
|
+
// type expressions (types) in between, e.g. `${A}_${B}` becomes:
|
|
500
|
+
// {quasis: ['', '_', ''], types: [<A>, <B>]}
|
|
501
|
+
const quasis = [node.head.text];
|
|
502
|
+
/** @type {any[]} */
|
|
503
|
+
const types = [];
|
|
504
|
+
for (const span of node.templateSpans) {
|
|
505
|
+
types.push(toSourceTS(span.type));
|
|
506
|
+
quasis.push(span.literal.text);
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
type: 'templateLiteral',
|
|
510
|
+
quasis,
|
|
511
|
+
types
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
case NoSubstitutionTemplateLiteral:
|
|
515
|
+
// A template literal type without interpolations, e.g. `id`
|
|
516
|
+
return {
|
|
517
|
+
type: 'templateLiteral',
|
|
518
|
+
quasis: [node.text],
|
|
519
|
+
types: []
|
|
520
|
+
};
|
|
417
521
|
case UnionType:
|
|
418
522
|
if (!ts.isUnionTypeNode(node)) {
|
|
419
523
|
throw Error("Impossible");
|
|
@@ -441,7 +545,13 @@ function toSourceTS(node) {
|
|
|
441
545
|
throw Error("Impossible");
|
|
442
546
|
}
|
|
443
547
|
const name = toSourceTS(member.name);
|
|
444
|
-
|
|
548
|
+
let type = toSourceTS(member.type);
|
|
549
|
+
if (member.questionToken) {
|
|
550
|
+
if (type && typeof type === 'object') type.optional = true;else type = {
|
|
551
|
+
type,
|
|
552
|
+
optional: true
|
|
553
|
+
};
|
|
554
|
+
}
|
|
445
555
|
properties[name] = type;
|
|
446
556
|
} else {
|
|
447
557
|
console.warn('TypeLiteral: unhandled member', member);
|
|
@@ -579,6 +689,71 @@ function toSourceTS(node) {
|
|
|
579
689
|
* @property {(object | string)[]} [elements] - For tuples.
|
|
580
690
|
* @property {object | string} [argument] - For typeof.
|
|
581
691
|
*/
|
|
692
|
+
/**
|
|
693
|
+
* Splits a string by a delimiter, ignoring delimiters nested inside <>, {}, [], ().
|
|
694
|
+
* @param {string} str - The string to split.
|
|
695
|
+
* @param {string} delimiter - Single character delimiter.
|
|
696
|
+
* @returns {string[]} Top-level split parts.
|
|
697
|
+
*/
|
|
698
|
+
function splitTopLevel(str, delimiter) {
|
|
699
|
+
const parts = [];
|
|
700
|
+
let depthAngle = 0;
|
|
701
|
+
let depthCurly = 0;
|
|
702
|
+
let depthSquare = 0;
|
|
703
|
+
let depthParen = 0;
|
|
704
|
+
let current = '';
|
|
705
|
+
for (const c of str) {
|
|
706
|
+
if (c === '<') depthAngle++;else if (c === '>') depthAngle--;else if (c === '{') depthCurly++;else if (c === '}') depthCurly--;else if (c === '[') depthSquare++;else if (c === ']') depthSquare--;else if (c === '(') depthParen++;else if (c === ')') depthParen--;
|
|
707
|
+
if (c === delimiter && depthAngle === 0 && depthCurly === 0 && depthSquare === 0 && depthParen === 0) {
|
|
708
|
+
parts.push(current);
|
|
709
|
+
current = '';
|
|
710
|
+
} else {
|
|
711
|
+
current += c;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
parts.push(current);
|
|
715
|
+
return parts;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Parses `Name<A, B>` into name + raw arg strings, respecting nested brackets.
|
|
719
|
+
* Returns undefined when input isn't a generic reference.
|
|
720
|
+
* @param {string} type - Trimmed type string.
|
|
721
|
+
* @returns {{name: string, args: string[]}|undefined} Parsed generic reference.
|
|
722
|
+
*/
|
|
723
|
+
function parseGenericReference(type) {
|
|
724
|
+
const openIndex = type.indexOf('<');
|
|
725
|
+
if (openIndex === -1 || !type.endsWith('>')) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const name = type.slice(0, openIndex).trim();
|
|
729
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$.]*$/.test(name)) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
// Find matching '>' for the first '<' to ensure outermost brackets wrap the whole type.
|
|
733
|
+
let depth = 0;
|
|
734
|
+
let closeIndex = -1;
|
|
735
|
+
for (let i = openIndex; i < type.length; i++) {
|
|
736
|
+
if (type[i] === '<') depth++;else if (type[i] === '>') {
|
|
737
|
+
depth--;
|
|
738
|
+
if (depth === 0) {
|
|
739
|
+
closeIndex = i;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (closeIndex !== type.length - 1) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const inner = type.slice(openIndex + 1, closeIndex);
|
|
748
|
+
const args = splitTopLevel(inner, ',').map(_ => _.trim()).filter(_ => _.length);
|
|
749
|
+
if (!args.length) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
name,
|
|
754
|
+
args
|
|
755
|
+
};
|
|
756
|
+
}
|
|
582
757
|
/**
|
|
583
758
|
* 'DepFree' refers to the fact that this function has no dependencies,
|
|
584
759
|
* while `expandType` depends on TypeScript itself for maximum compatibility.
|
|
@@ -594,6 +769,26 @@ function toSourceTS(node) {
|
|
|
594
769
|
*/
|
|
595
770
|
function expandTypeDepFree(type) {
|
|
596
771
|
type = type.trim();
|
|
772
|
+
// JSDocNullableType (`T?` / `?T`): union with null, matching expandType().
|
|
773
|
+
if (type.endsWith('?') && type.length > 1) {
|
|
774
|
+
return {
|
|
775
|
+
type: 'union',
|
|
776
|
+
members: [expandTypeDepFree(type.slice(0, -1).trim()), 'null']
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
if (type.startsWith('?') && type.length > 1) {
|
|
780
|
+
return {
|
|
781
|
+
type: 'union',
|
|
782
|
+
members: [expandTypeDepFree(type.slice(1).trim()), 'null']
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
// `readonly T` erased at runtime, same shape as the inner type.
|
|
786
|
+
if (type.startsWith('readonly ') && type.length > 9) {
|
|
787
|
+
return expandTypeDepFree(type.slice(9).trim());
|
|
788
|
+
}
|
|
789
|
+
if (type === 'unique symbol') {
|
|
790
|
+
return 'any';
|
|
791
|
+
}
|
|
597
792
|
// '(123)' -> '123'
|
|
598
793
|
while (!type.includes('|') && type[0] === '(' && type[type.length - 1] === ')') {
|
|
599
794
|
type = type.slice(1, -1).trim();
|
|
@@ -640,6 +835,39 @@ function expandTypeDepFree(type) {
|
|
|
640
835
|
val: expandTypeDepFree(val)
|
|
641
836
|
};
|
|
642
837
|
}
|
|
838
|
+
// (3b) Map<...> / Set<...' for dep-free parity with expandType()
|
|
839
|
+
if (type.startsWith("Map<") && type.endsWith('>')) {
|
|
840
|
+
const inner = type.slice(4, -1);
|
|
841
|
+
const parts = splitTopLevel(inner, ',');
|
|
842
|
+
if (parts.length === 2) {
|
|
843
|
+
return {
|
|
844
|
+
type: "map",
|
|
845
|
+
key: expandTypeDepFree(parts[0].trim()),
|
|
846
|
+
val: expandTypeDepFree(parts[1].trim())
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (type.startsWith("Set<") && type.endsWith('>')) {
|
|
851
|
+
const inner = type.slice(4, -1);
|
|
852
|
+
return {
|
|
853
|
+
type: "set",
|
|
854
|
+
elementType: expandTypeDepFree(inner.trim())
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
// (3c) Generic reference types like ArrayLike<T>, ReadonlyArray<T> etc.
|
|
858
|
+
// Keep structured so the runtime can validate them instead of warning 'unchecked'.
|
|
859
|
+
const genericRef = parseGenericReference(type);
|
|
860
|
+
if (genericRef) {
|
|
861
|
+
const {
|
|
862
|
+
name,
|
|
863
|
+
args
|
|
864
|
+
} = genericRef;
|
|
865
|
+
return {
|
|
866
|
+
type: 'reference',
|
|
867
|
+
name,
|
|
868
|
+
args: args.map(expandTypeDepFree)
|
|
869
|
+
};
|
|
870
|
+
}
|
|
643
871
|
// (4) {...}
|
|
644
872
|
if (type[0] === '{' && type[type.length - 1] === '}') {
|
|
645
873
|
const propertiesArray = type.slice(1, -1).split(','); // ['entity: Entity', ' app: AppBase']
|
|
@@ -697,96 +925,302 @@ function expandTypeDepFree(type) {
|
|
|
697
925
|
properties: {}
|
|
698
926
|
};
|
|
699
927
|
}
|
|
928
|
+
// Literal normalization for parity with expandType():
|
|
929
|
+
// numeric literals become numbers, true/false become booleans.
|
|
930
|
+
if (type === 'true') {
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
if (type === 'false') {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(type)) {
|
|
937
|
+
return Number(type);
|
|
938
|
+
}
|
|
700
939
|
return type;
|
|
701
940
|
}
|
|
702
941
|
|
|
703
|
-
/** @typedef {import('@babel/types').Node} Node */
|
|
704
|
-
/** @typedef {import('@babel/types').Function} Function */
|
|
705
942
|
/**
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
943
|
+
* Infers a parameter type from its default value AST node.
|
|
944
|
+
* Returns widened types like TypeScript does (`= 0` means `number`, not
|
|
945
|
+
* literal `0`; `= null` widens to `any`).
|
|
946
|
+
* Returns `undefined` when nothing useful can be inferred — the caller then
|
|
947
|
+
* emits no check, exactly like an undocumented parameter today.
|
|
948
|
+
* Shapes match what `expandType` produces so they can be embedded as-is.
|
|
949
|
+
* @param {import('@babel/types').Node} node - The default value AST node.
|
|
950
|
+
* @returns {string | object | undefined} Inferred type or `undefined` to skip.
|
|
710
951
|
*/
|
|
711
|
-
function
|
|
952
|
+
function inferTypeFromDefault$1(node) {
|
|
953
|
+
if (!node) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
712
956
|
switch (node.type) {
|
|
957
|
+
case 'NumericLiteral':
|
|
958
|
+
return 'number';
|
|
959
|
+
case 'StringLiteral':
|
|
960
|
+
return 'string';
|
|
961
|
+
case 'BooleanLiteral':
|
|
962
|
+
return 'boolean';
|
|
963
|
+
case 'BigIntLiteral':
|
|
964
|
+
return {
|
|
965
|
+
type: 'bigint'
|
|
966
|
+
};
|
|
967
|
+
case 'RegExpLiteral':
|
|
968
|
+
return 'RegExp';
|
|
969
|
+
case 'TemplateLiteral':
|
|
970
|
+
return 'string';
|
|
971
|
+
case 'ArrayExpression':
|
|
972
|
+
return {
|
|
973
|
+
type: 'array',
|
|
974
|
+
elementType: 'any'
|
|
975
|
+
};
|
|
976
|
+
case 'ObjectExpression':
|
|
977
|
+
return {
|
|
978
|
+
type: 'object',
|
|
979
|
+
properties: {}
|
|
980
|
+
};
|
|
713
981
|
case 'ArrowFunctionExpression':
|
|
714
|
-
case 'ClassMethod':
|
|
715
|
-
case 'ClassPrivateMethod':
|
|
716
|
-
case 'FunctionDeclaration':
|
|
717
982
|
case 'FunctionExpression':
|
|
718
|
-
|
|
719
|
-
|
|
983
|
+
return 'Function';
|
|
984
|
+
case 'NewExpression':
|
|
985
|
+
{
|
|
986
|
+
const {
|
|
987
|
+
callee
|
|
988
|
+
} = node;
|
|
989
|
+
if (callee.type === 'Identifier') {
|
|
990
|
+
return callee.name;
|
|
991
|
+
}
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
case 'UnaryExpression':
|
|
995
|
+
{
|
|
996
|
+
const {
|
|
997
|
+
operator,
|
|
998
|
+
argument
|
|
999
|
+
} = node;
|
|
1000
|
+
if (operator === '!') {
|
|
1001
|
+
return 'boolean';
|
|
1002
|
+
}
|
|
1003
|
+
if (operator === 'void' || operator === 'typeof') {
|
|
1004
|
+
return operator === 'void' ? 'undefined' : 'string';
|
|
1005
|
+
}
|
|
1006
|
+
if ((operator === '-' || operator === '+') && argument.type === 'NumericLiteral') {
|
|
1007
|
+
return 'number';
|
|
1008
|
+
}
|
|
1009
|
+
if ((operator === '-' || operator === '+') && argument.type === 'BigIntLiteral') {
|
|
1010
|
+
return {
|
|
1011
|
+
type: 'bigint'
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
720
1016
|
}
|
|
721
|
-
return false;
|
|
722
1017
|
}
|
|
723
1018
|
|
|
724
1019
|
/**
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
* @
|
|
1020
|
+
* Extracts the parameter name and its optionality from a JSDoc parameter string.
|
|
1021
|
+
*
|
|
1022
|
+
* This function takes a rest parameter string from a JSDoc comment, trims it, and determines the parameter's
|
|
1023
|
+
* name and whether it is optional. The optionality is inferred based on the presence of square brackets around
|
|
1024
|
+
* the parameter name.
|
|
1025
|
+
*
|
|
1026
|
+
* @param {string} rest - The rest part of a JSDoc parameter string to parse.
|
|
1027
|
+
* @returns {[string, boolean]} A tuple where the first element is the name of the parameter,
|
|
1028
|
+
* and the second element is a boolean indicating if the parameter is optional.
|
|
732
1029
|
*/
|
|
733
|
-
function
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
};
|
|
747
|
-
} else {
|
|
748
|
-
debugger;
|
|
749
|
-
console.warn("simplifyType> neither object nor string for type", type);
|
|
750
|
-
}
|
|
751
|
-
if (type.type === 'object' && type.properties && Object.keys(type.properties).length === 0) {
|
|
752
|
-
delete type.properties;
|
|
753
|
-
// console.log("delete empty", type);
|
|
1030
|
+
function extractNameAndOptionality(rest) {
|
|
1031
|
+
rest = rest.trim();
|
|
1032
|
+
let optional = false;
|
|
1033
|
+
// Examples:
|
|
1034
|
+
// name: [kwargs={}] The configuration parameters.
|
|
1035
|
+
// name: [d = 1.0] Sample spacing
|
|
1036
|
+
if (rest[0] === '[') {
|
|
1037
|
+
// Possible improvement: counting opening/closing brackets for perfect match
|
|
1038
|
+
const closer = rest.lastIndexOf(']');
|
|
1039
|
+
// Afterwards name will be: d = 1.0
|
|
1040
|
+
rest = rest.substring(1, closer);
|
|
1041
|
+
// mark it for the type:
|
|
1042
|
+
optional = true;
|
|
754
1043
|
}
|
|
755
|
-
|
|
756
|
-
|
|
1044
|
+
// Strip the rest (either leftover of optional value or description)
|
|
1045
|
+
const name = rest.split(' ')[0].split('=')[0].trim();
|
|
1046
|
+
return [name, optional];
|
|
757
1047
|
}
|
|
758
1048
|
|
|
759
1049
|
/**
|
|
760
|
-
*
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
* @
|
|
764
|
-
* @
|
|
1050
|
+
* Extracts the content of a string that is delimited by curly braces.
|
|
1051
|
+
* @example
|
|
1052
|
+
* extractCurlyContent('{ {inner} }'); // Returns: {content: ' {inner} ', nextIndex: 11}
|
|
1053
|
+
* @param {string} line - The string to extract from.
|
|
1054
|
+
* @returns {{content: string, nextIndex: number}} An object containing the extracted content,
|
|
1055
|
+
* and the index of the character immediately following the closing curly brace.
|
|
765
1056
|
*/
|
|
1057
|
+
function extractCurlyContent(line) {
|
|
1058
|
+
const firstCurly = line.indexOf('{');
|
|
1059
|
+
let k = firstCurly + 1;
|
|
1060
|
+
let count = 0;
|
|
1061
|
+
for (; k < line.length; k++) {
|
|
1062
|
+
const c = line[k];
|
|
1063
|
+
if (c === '{') {
|
|
1064
|
+
count++;
|
|
1065
|
+
} else if (c === '}') {
|
|
1066
|
+
count--;
|
|
1067
|
+
}
|
|
1068
|
+
if (count === -1) {
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const content = line.substring(firstCurly + 1, k);
|
|
1073
|
+
return {
|
|
1074
|
+
content,
|
|
1075
|
+
nextIndex: k + 1
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
766
1078
|
/**
|
|
767
|
-
* Parses JSDoc comments to extract
|
|
1079
|
+
* Parses JSDoc comments to extract and expand typedefs and their associated properties.
|
|
768
1080
|
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
1081
|
+
* It iterates through the lines of a `CommentBlock` from the Babel AST, looking for `@typedef` and `@property`
|
|
1082
|
+
* annotations. When it finds a typedef, it stores it in the `typedefs` record. When it finds a property,
|
|
1083
|
+
* it adds it to the last found typedef if it is an object type.
|
|
1084
|
+
* @param {Record<string, object>} typedefs - An object to store typedefs, mapping type names to their expanded definitions.
|
|
1085
|
+
* @param {Console["warn"]} warn - A warn function used for emitting warnings about non-extensible types.
|
|
1086
|
+
* @param {import("@babel/types").Comment} comment - A comment extracted from Babel's AST, expected to be a CommentBlock containing type definitions.
|
|
1087
|
+
* @param {Function} expandType - A function that takes a type expression as a string and returns a structured representation of the type.
|
|
772
1088
|
*/
|
|
773
|
-
function
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1089
|
+
function parseJSDocTypedef(typedefs, warn, comment, expandType) {
|
|
1090
|
+
const {
|
|
1091
|
+
type,
|
|
1092
|
+
value
|
|
1093
|
+
} = comment;
|
|
1094
|
+
if (type !== 'CommentBlock') {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const lines = value.split('\n');
|
|
1098
|
+
let lastTypedef;
|
|
1099
|
+
for (let line of lines) {
|
|
1100
|
+
line = line.trim();
|
|
1101
|
+
if (line[0] === '*') {
|
|
1102
|
+
line = line.slice(1).trim();
|
|
1103
|
+
}
|
|
1104
|
+
if (line.startsWith('@typedef')) {
|
|
1105
|
+
const {
|
|
1106
|
+
content: def,
|
|
1107
|
+
nextIndex
|
|
1108
|
+
} = extractCurlyContent(line);
|
|
1109
|
+
let name = line.substring(nextIndex).trim();
|
|
1110
|
+
// Drop description
|
|
1111
|
+
name = name.split(' ')[0];
|
|
1112
|
+
lastTypedef = expandType(def);
|
|
1113
|
+
// Ignore @typedef's that only refer to themselves in another file (see typedef-overwrite test)
|
|
1114
|
+
if (lastTypedef !== name) {
|
|
1115
|
+
typedefs[name] = lastTypedef;
|
|
1116
|
+
}
|
|
1117
|
+
} else if (line.startsWith('@property')) {
|
|
1118
|
+
var _lastTypedef;
|
|
1119
|
+
// class @property
|
|
1120
|
+
if (!lastTypedef) {
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
const {
|
|
1124
|
+
content,
|
|
1125
|
+
nextIndex
|
|
1126
|
+
} = extractCurlyContent(line);
|
|
1127
|
+
const rest = line.substring(nextIndex);
|
|
1128
|
+
const propType = expandType(content);
|
|
1129
|
+
const [name, optional] = extractNameAndOptionality(rest);
|
|
1130
|
+
// console.log({name, optional, propType});
|
|
1131
|
+
const finalType = annotateOptional(propType, optional);
|
|
1132
|
+
if (((_lastTypedef = lastTypedef) == null ? void 0 : _lastTypedef.type) === 'object') {
|
|
1133
|
+
lastTypedef.properties[name] = finalType;
|
|
1134
|
+
} else {
|
|
1135
|
+
warn("not an extensible type", lastTypedef);
|
|
1136
|
+
}
|
|
1137
|
+
} else if (line.startsWith('@callback')) {
|
|
1138
|
+
const name = line.substring(9).trim();
|
|
1139
|
+
typedefs[name] = 'Function';
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Extracts an inline `/** @type {X} *\/` annotation from parameter comments,
|
|
1146
|
+
* e.g. `function add(/** @type {number} *\/ a) {...}`.
|
|
1147
|
+
* @param {Array<{value: string}>|undefined} leadingComments - Leading comments of a param node.
|
|
1148
|
+
* @param {Function} expandType - Function expanding a type string.
|
|
1149
|
+
* @returns {any} Expanded type or `undefined` when no inline `@type` found.
|
|
1150
|
+
*/
|
|
1151
|
+
function parseInlineParamType(leadingComments, expandType) {
|
|
1152
|
+
if (!Array.isArray(leadingComments)) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
for (let i = leadingComments.length - 1; i >= 0; i--) {
|
|
1156
|
+
const {
|
|
1157
|
+
value
|
|
1158
|
+
} = leadingComments[i];
|
|
1159
|
+
if (!value || !value.includes('@type')) {
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
const {
|
|
1163
|
+
content
|
|
1164
|
+
} = extractCurlyContent(value.slice(value.indexOf('@type')));
|
|
1165
|
+
if (!content || !content.trim()) {
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
return expandType(content.trim());
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/** @typedef {import('@babel/types').Node} Node */
|
|
1173
|
+
/** @typedef {import('@babel/types').Function} Function */
|
|
1174
|
+
/**
|
|
1175
|
+
* Checks if the provided node is a function-like structure.
|
|
1176
|
+
*
|
|
1177
|
+
* @param {Node} node - The Babel AST node to be tested.
|
|
1178
|
+
* @returns {node is Function} - `true` if the node is a function-like structure, otherwise `false`.
|
|
1179
|
+
*/
|
|
1180
|
+
function nodeIsFunctionLike(node) {
|
|
1181
|
+
switch (node.type) {
|
|
1182
|
+
case 'ArrowFunctionExpression':
|
|
1183
|
+
case 'ClassMethod':
|
|
1184
|
+
case 'ClassPrivateMethod':
|
|
1185
|
+
case 'FunctionDeclaration':
|
|
1186
|
+
case 'FunctionExpression':
|
|
1187
|
+
case 'ObjectMethod':
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* @typedef {ReturnType<typeof parseJSDoc>} ParseJSDocReturnType
|
|
1195
|
+
*/
|
|
1196
|
+
/**
|
|
1197
|
+
* @typedef {typeof expandTypeDepFree} ExpandType
|
|
1198
|
+
* @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
|
|
1199
|
+
*/
|
|
1200
|
+
/**
|
|
1201
|
+
* Parses JSDoc comments to extract parameter type information.
|
|
1202
|
+
*
|
|
1203
|
+
* @param {string} src - The JSDoc comment string to parse.
|
|
1204
|
+
* @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
|
|
1205
|
+
* @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
|
|
1206
|
+
*/
|
|
1207
|
+
function parseJSDoc(src, expandType = expandTypeDepFree) {
|
|
1208
|
+
// Parse something like: @param {Object} [kwargs={}] Optional arguments.
|
|
1209
|
+
const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\-\{\}\.'" ]+)/g;
|
|
1210
|
+
const matches = [...src.matchAll(regex)];
|
|
1211
|
+
/** @type {Record<string, ExpandTypeReturnType>} */
|
|
1212
|
+
const params = Object.create(null);
|
|
1213
|
+
matches.forEach(_ => {
|
|
1214
|
+
const type = expandType(_[1].trim());
|
|
1215
|
+
let name = _[2].trim();
|
|
1216
|
+
let optional = false;
|
|
1217
|
+
// Examples:
|
|
1218
|
+
// name: [kwargs={}] The configuration parameters.
|
|
1219
|
+
// name: [d = 1.0] Sample spacing
|
|
1220
|
+
if (name[0] === '[') {
|
|
1221
|
+
// Counting opening/closing brackets for perfect match
|
|
1222
|
+
let openCloseCount = 1;
|
|
1223
|
+
let i = 1;
|
|
790
1224
|
for (; i < name.length; i++) {
|
|
791
1225
|
const c = name[i];
|
|
792
1226
|
if (c === '[') {
|
|
@@ -805,16 +1239,16 @@ function parseJSDoc(src, expandType = expandTypeDepFree) {
|
|
|
805
1239
|
}
|
|
806
1240
|
// Strip the rest (either leftover of optional value or description)
|
|
807
1241
|
name = name.split(' ')[0].split('=')[0].trim();
|
|
808
|
-
const
|
|
1242
|
+
const annotatedType = annotateOptional(type, optional);
|
|
809
1243
|
// Turn "options.stats[].unitsName" into ['options', 'stats', 'unitsName'].
|
|
810
1244
|
const parts = name.split(/[\[\]]*\./);
|
|
811
1245
|
let properties = params;
|
|
812
1246
|
for (const part of parts) {
|
|
813
1247
|
const toptype = properties[part];
|
|
814
1248
|
if (!toptype) {
|
|
815
|
-
// No toptype means we resolved as far as possible, now we can add `
|
|
1249
|
+
// No toptype means we resolved as far as possible, now we can add `annotatedType`.
|
|
816
1250
|
console.assert(part === parts.at(-1), 'Current part and last part should be the same.');
|
|
817
|
-
properties[part] =
|
|
1251
|
+
properties[part] = annotatedType;
|
|
818
1252
|
} else if (toptype.type === "union") {
|
|
819
1253
|
const typeObject = toptype.members.find(_ => (_ == null ? void 0 : _.type) === 'object');
|
|
820
1254
|
properties = typeObject.properties;
|
|
@@ -828,7 +1262,7 @@ function parseJSDoc(src, expandType = expandTypeDepFree) {
|
|
|
828
1262
|
src,
|
|
829
1263
|
toptype,
|
|
830
1264
|
parts,
|
|
831
|
-
|
|
1265
|
+
annotatedType
|
|
832
1266
|
});
|
|
833
1267
|
}
|
|
834
1268
|
}
|
|
@@ -851,8 +1285,8 @@ function parseJSDocSetter(src, expandType = expandTypeDepFree) {
|
|
|
851
1285
|
if (matches.length === 1) {
|
|
852
1286
|
const match = matches[0];
|
|
853
1287
|
const type = expandType(match[1]);
|
|
854
|
-
const
|
|
855
|
-
return
|
|
1288
|
+
const annotatedType = annotateOptional(type, /* optional */false);
|
|
1289
|
+
return annotatedType;
|
|
856
1290
|
}
|
|
857
1291
|
}
|
|
858
1292
|
|
|
@@ -884,129 +1318,99 @@ function parseJSDocTemplates(src, expandType = expandTypeDepFree) {
|
|
|
884
1318
|
return templates;
|
|
885
1319
|
}
|
|
886
1320
|
|
|
1321
|
+
function _extends() {
|
|
1322
|
+
_extends = Object.assign ? Object.assign.bind() : function (target) {
|
|
1323
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
1324
|
+
var source = arguments[i];
|
|
1325
|
+
for (var key in source) {
|
|
1326
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1327
|
+
target[key] = source[key];
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return target;
|
|
1332
|
+
};
|
|
1333
|
+
return _extends.apply(this, arguments);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
887
1336
|
/**
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
* @param {string} rest - The rest part of a JSDoc parameter string to parse.
|
|
895
|
-
* @returns {[string, boolean]} A tuple where the first element is the name of the parameter,
|
|
896
|
-
* and the second element is a boolean indicating if the parameter is optional.
|
|
1337
|
+
* Returns a new object with each value mapped by `fn`, leaving the original
|
|
1338
|
+
* untouched. Returns `obj` as-is if it is falsy.
|
|
1339
|
+
* @param {Record<string, any>} obj - The source object.
|
|
1340
|
+
* @param {(value: any, key: string) => any} fn - Mapping function.
|
|
1341
|
+
* @returns {Record<string, any>} A new object with mapped values.
|
|
897
1342
|
*/
|
|
898
|
-
function
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
// name: [d = 1.0] Sample spacing
|
|
904
|
-
if (rest[0] === '[') {
|
|
905
|
-
// Possible improvement: counting opening/closing brackets for perfect match
|
|
906
|
-
const closer = rest.lastIndexOf(']');
|
|
907
|
-
// Afterwards name will be: d = 1.0
|
|
908
|
-
rest = rest.substring(1, closer);
|
|
909
|
-
// mark it for the type:
|
|
910
|
-
optional = true;
|
|
1343
|
+
function mapValues(obj, fn) {
|
|
1344
|
+
if (!obj) return obj;
|
|
1345
|
+
const result = {};
|
|
1346
|
+
for (const key in obj) {
|
|
1347
|
+
result[key] = fn(obj[key], key);
|
|
911
1348
|
}
|
|
912
|
-
|
|
913
|
-
const name = rest.split(' ')[0].split('=')[0].trim();
|
|
914
|
-
return [name, optional];
|
|
1349
|
+
return result;
|
|
915
1350
|
}
|
|
916
1351
|
|
|
917
1352
|
/**
|
|
918
|
-
*
|
|
919
|
-
* @
|
|
920
|
-
* extractCurlyContent('{ {inner} }'); // Returns: {content: ' {inner} ', nextIndex: 11}
|
|
921
|
-
* @param {string} line - The string to extract from.
|
|
922
|
-
* @returns {{content: string, nextIndex: number}} An object containing the extracted content,
|
|
923
|
-
* and the index of the character immediately following the closing curly brace.
|
|
1353
|
+
* @typedef DocType
|
|
1354
|
+
* @property {boolean} optional - Type is optional.
|
|
924
1355
|
*/
|
|
925
|
-
function extractCurlyContent(line) {
|
|
926
|
-
const firstCurly = line.indexOf('{');
|
|
927
|
-
let k = firstCurly + 1;
|
|
928
|
-
let count = 0;
|
|
929
|
-
for (; k < line.length; k++) {
|
|
930
|
-
const c = line[k];
|
|
931
|
-
if (c === '{') {
|
|
932
|
-
count++;
|
|
933
|
-
} else if (c === '}') {
|
|
934
|
-
count--;
|
|
935
|
-
}
|
|
936
|
-
if (count === -1) {
|
|
937
|
-
break;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
const content = line.substring(firstCurly + 1, k);
|
|
941
|
-
return {
|
|
942
|
-
content,
|
|
943
|
-
nextIndex: k + 1
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
1356
|
/**
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
* @
|
|
953
|
-
* @param {Console["warn"]} warn - A warn function used for emitting warnings about non-extensible types.
|
|
954
|
-
* @param {import("@babel/types").Comment} comment - A comment extracted from Babel's AST, expected to be a CommentBlock containing type definitions.
|
|
955
|
-
* @param {Function} expandType - A function that takes a type expression as a string and returns a structured representation of the type.
|
|
1357
|
+
* Recursively clones and simplifies a type for emission into source code.
|
|
1358
|
+
* Strips empty `properties` and collapses empty `object` types to the bare
|
|
1359
|
+
* string `'object'`. Numbers/booleans (literal types) pass through.
|
|
1360
|
+
* Non-destructive — the original type tree is never mutated.
|
|
1361
|
+
* @param {string | DocType | number | boolean} type - The type.
|
|
1362
|
+
* @returns {string | DocType | number | boolean} The simplified type.
|
|
956
1363
|
*/
|
|
957
|
-
function
|
|
958
|
-
|
|
959
|
-
type
|
|
960
|
-
value
|
|
961
|
-
} = comment;
|
|
962
|
-
if (type !== 'CommentBlock') {
|
|
963
|
-
return;
|
|
1364
|
+
function simplifyType(type) {
|
|
1365
|
+
if (!(type instanceof Object)) {
|
|
1366
|
+
return type;
|
|
964
1367
|
}
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
line = line.slice(1).trim();
|
|
971
|
-
}
|
|
972
|
-
if (line.startsWith('@typedef')) {
|
|
973
|
-
const {
|
|
974
|
-
content: def,
|
|
975
|
-
nextIndex
|
|
976
|
-
} = extractCurlyContent(line);
|
|
977
|
-
let name = line.substring(nextIndex).trim();
|
|
978
|
-
// Drop description
|
|
979
|
-
name = name.split(' ')[0];
|
|
980
|
-
lastTypedef = expandType(def);
|
|
981
|
-
// Ignore @typedef's that only refer to themselves in another file (see typedef-overwrite test)
|
|
982
|
-
if (lastTypedef !== name) {
|
|
983
|
-
typedefs[name] = lastTypedef;
|
|
984
|
-
}
|
|
985
|
-
} else if (line.startsWith('@property')) {
|
|
986
|
-
var _lastTypedef;
|
|
987
|
-
// class @property
|
|
988
|
-
if (!lastTypedef) {
|
|
989
|
-
continue;
|
|
990
|
-
}
|
|
991
|
-
const {
|
|
992
|
-
content,
|
|
993
|
-
nextIndex
|
|
994
|
-
} = extractCurlyContent(line);
|
|
995
|
-
const rest = line.substring(nextIndex);
|
|
996
|
-
const propType = expandType(content);
|
|
997
|
-
const [name, optional] = extractNameAndOptionality(rest);
|
|
998
|
-
// console.log({name, optional, propType});
|
|
999
|
-
const finalType = simplifyType(propType, optional);
|
|
1000
|
-
if (((_lastTypedef = lastTypedef) == null ? void 0 : _lastTypedef.type) === 'object') {
|
|
1001
|
-
lastTypedef.properties[name] = finalType;
|
|
1002
|
-
} else {
|
|
1003
|
-
warn("not an extensible type", lastTypedef);
|
|
1004
|
-
}
|
|
1005
|
-
} else if (line.startsWith('@callback')) {
|
|
1006
|
-
const name = line.substring(9).trim();
|
|
1007
|
-
typedefs[name] = 'Function';
|
|
1368
|
+
const out = _extends({}, type);
|
|
1369
|
+
if (out.properties) {
|
|
1370
|
+
out.properties = mapValues(out.properties, simplifyType);
|
|
1371
|
+
if (out.type === 'object' && !Object.keys(out.properties).length) {
|
|
1372
|
+
delete out.properties;
|
|
1008
1373
|
}
|
|
1009
1374
|
}
|
|
1375
|
+
if (out.indexSignatures && Array.isArray(out.indexSignatures)) {
|
|
1376
|
+
out.indexSignatures = out.indexSignatures.map(simplifyType);
|
|
1377
|
+
}
|
|
1378
|
+
if (out.type === 'union' && out.members) {
|
|
1379
|
+
out.members = out.members.map(simplifyType);
|
|
1380
|
+
}
|
|
1381
|
+
if (out.type === 'array' && out.elementType) {
|
|
1382
|
+
out.elementType = simplifyType(out.elementType);
|
|
1383
|
+
}
|
|
1384
|
+
if (out.type === 'tuple' && out.elements) {
|
|
1385
|
+
out.elements = out.elements.map(simplifyType);
|
|
1386
|
+
}
|
|
1387
|
+
if (out.type === 'promise' && out.elementType) {
|
|
1388
|
+
out.elementType = simplifyType(out.elementType);
|
|
1389
|
+
}
|
|
1390
|
+
if (out.type === 'reference' && Array.isArray(out.args)) {
|
|
1391
|
+
out.args = out.args.map(simplifyType);
|
|
1392
|
+
}
|
|
1393
|
+
if (out.type === 'record') {
|
|
1394
|
+
if (out.key) out.key = simplifyType(out.key);
|
|
1395
|
+
if (out.val) out.val = simplifyType(out.val);
|
|
1396
|
+
}
|
|
1397
|
+
if (out.type === 'typeof' && out.argument) {
|
|
1398
|
+
out.argument = simplifyType(out.argument);
|
|
1399
|
+
}
|
|
1400
|
+
if (out.type === 'object' && !out.properties && !out.indexSignatures && !out.optional) {
|
|
1401
|
+
return 'object';
|
|
1402
|
+
}
|
|
1403
|
+
return out;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Returns the pretty-printed JSON of the simplified type for embedding
|
|
1408
|
+
* into generated source code.
|
|
1409
|
+
* @param {import('./simplifyType.js').DocType | string | number | boolean} type - The type.
|
|
1410
|
+
* @returns {string} JSON source ready for code generation.
|
|
1411
|
+
*/
|
|
1412
|
+
function simplifyTypeToSource(type) {
|
|
1413
|
+
return JSON.stringify(simplifyType(type), null, 2);
|
|
1010
1414
|
}
|
|
1011
1415
|
|
|
1012
1416
|
/**
|
|
@@ -1345,7 +1749,8 @@ class Stringifier {
|
|
|
1345
1749
|
return '';
|
|
1346
1750
|
}
|
|
1347
1751
|
/**
|
|
1348
|
-
*
|
|
1752
|
+
* A string of two spaces per indentation.
|
|
1753
|
+
* @type {string}
|
|
1349
1754
|
*/
|
|
1350
1755
|
get spaces() {
|
|
1351
1756
|
return ' '.repeat(this.numSpaces);
|
|
@@ -3271,7 +3676,7 @@ class Asserter extends Stringifier {
|
|
|
3271
3676
|
return '';
|
|
3272
3677
|
}
|
|
3273
3678
|
let header = super.getHeader();
|
|
3274
|
-
header += "import {inspectType, inspectTypeWithTemplates, youCanAddABreakpointHere, registerVariable";
|
|
3679
|
+
header += "import {inspectIndexedAccess, inspectType, inspectTypeWithTemplates, youCanAddABreakpointHere, registerVariable";
|
|
3275
3680
|
if (this.validateDivision) {
|
|
3276
3681
|
header += ", validateDivision";
|
|
3277
3682
|
}
|
|
@@ -3308,10 +3713,21 @@ class Asserter extends Stringifier {
|
|
|
3308
3713
|
//if (parent.type === 'CallExpression') {
|
|
3309
3714
|
// break;
|
|
3310
3715
|
//}
|
|
3311
|
-
if (
|
|
3716
|
+
if (nodeIsFunctionLike(parent)) {
|
|
3312
3717
|
break;
|
|
3313
3718
|
}
|
|
3314
3719
|
if (parent.leadingComments) {
|
|
3720
|
+
if (parent.type === 'VariableDeclaration') {
|
|
3721
|
+
const comments = parent.leadingComments;
|
|
3722
|
+
const lastComment = comments[comments.length - 1];
|
|
3723
|
+
const isStatementDoc = Boolean(parent.loc && (lastComment == null ? void 0 : lastComment.loc) && lastComment.loc.end.line + 1 === parent.loc.start.line);
|
|
3724
|
+
if (isStatementDoc) {
|
|
3725
|
+
const declarator = parents[i + 1];
|
|
3726
|
+
if (!declarator || declarator.type !== 'VariableDeclarator' || declarator.init !== node) {
|
|
3727
|
+
break;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3315
3731
|
return parent;
|
|
3316
3732
|
}
|
|
3317
3733
|
i--;
|
|
@@ -3346,10 +3762,24 @@ class Asserter extends Stringifier {
|
|
|
3346
3762
|
//if (parent.type === 'CallExpression') {
|
|
3347
3763
|
// break;
|
|
3348
3764
|
//}
|
|
3349
|
-
if (
|
|
3765
|
+
if (nodeIsFunctionLike(parent)) {
|
|
3350
3766
|
break;
|
|
3351
3767
|
}
|
|
3352
3768
|
if (parent.leadingComments) {
|
|
3769
|
+
if (parent.type === 'VariableDeclaration') {
|
|
3770
|
+
const comments = parent.leadingComments;
|
|
3771
|
+
const lastComment = comments[comments.length - 1];
|
|
3772
|
+
const isStatementDoc = Boolean(parent.loc && (lastComment == null ? void 0 : lastComment.loc) && lastComment.loc.end.line + 1 === parent.loc.start.line);
|
|
3773
|
+
if (isStatementDoc) {
|
|
3774
|
+
// Docblock directly above the statement: TypeScript-like cascade,
|
|
3775
|
+
// only direct declarator initializers inherit it. Functions nested
|
|
3776
|
+
// deeper (e.g. inside object literals) do not.
|
|
3777
|
+
const declarator = parents[i + 1];
|
|
3778
|
+
if (!declarator || declarator.type !== 'VariableDeclarator' || declarator.init !== node) {
|
|
3779
|
+
break;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3353
3783
|
return parent;
|
|
3354
3784
|
}
|
|
3355
3785
|
i--;
|
|
@@ -3497,7 +3927,7 @@ class Asserter extends Stringifier {
|
|
|
3497
3927
|
const {
|
|
3498
3928
|
stats
|
|
3499
3929
|
} = this;
|
|
3500
|
-
const type =
|
|
3930
|
+
const type = nodeIsFunctionLike(node) ? node.type : this.parentType;
|
|
3501
3931
|
if (type === 'ClassMethod') {
|
|
3502
3932
|
const parent = /** @type {ClassMethod} */
|
|
3503
3933
|
this.parent;
|
|
@@ -3585,15 +4015,26 @@ class Asserter extends Stringifier {
|
|
|
3585
4015
|
const {
|
|
3586
4016
|
parent
|
|
3587
4017
|
} = this;
|
|
3588
|
-
if (node.type === 'BlockStatement' && !
|
|
4018
|
+
if (node.type === 'BlockStatement' && !nodeIsFunctionLike(parent)) {
|
|
3589
4019
|
return '';
|
|
3590
4020
|
}
|
|
3591
4021
|
const jsdoc = this.getJSDoc(node);
|
|
3592
4022
|
// return '// ' + JSON.stringify(jsdoc) + '\n';
|
|
3593
4023
|
const stat = this.getStatsForNode(node);
|
|
3594
4024
|
if (!jsdoc) {
|
|
3595
|
-
|
|
3596
|
-
|
|
4025
|
+
// No JSDoc at all: default-value inference is the only
|
|
4026
|
+
// source of types. Emit nothing when nothing is inferable.
|
|
4027
|
+
const inferred = this.collectDefaultChecks(node, new Set());
|
|
4028
|
+
if (!inferred.length) {
|
|
4029
|
+
stat.unchecked++;
|
|
4030
|
+
return '';
|
|
4031
|
+
}
|
|
4032
|
+
const _loc = this.getName(node);
|
|
4033
|
+
if (this.ignoreLocations.includes(_loc)) {
|
|
4034
|
+
return '// IGNORE RTI TYPE VALIDATIONS, KNOWN ISSUES\n';
|
|
4035
|
+
}
|
|
4036
|
+
stat.checked++;
|
|
4037
|
+
return this.emitDefaultChecks(node, inferred, true);
|
|
3597
4038
|
}
|
|
3598
4039
|
const {
|
|
3599
4040
|
templates,
|
|
@@ -3659,7 +4100,7 @@ class Asserter extends Stringifier {
|
|
|
3659
4100
|
// via arguments[paramIndex] anyway.
|
|
3660
4101
|
name = `arguments[${paramIndex}]`;
|
|
3661
4102
|
} else if (param.type === 'AssignmentPattern') {
|
|
3662
|
-
const
|
|
4103
|
+
const _loc2 = this.getName(node);
|
|
3663
4104
|
if (param.left.type === 'ArrayPattern' && type.type === 'array') {
|
|
3664
4105
|
// Add a type assertion for each element of the ArrayPattern
|
|
3665
4106
|
for (const element of param.left.elements) {
|
|
@@ -3674,12 +4115,12 @@ class Asserter extends Stringifier {
|
|
|
3674
4115
|
this.warn('Only Identifier case handled right now');
|
|
3675
4116
|
continue;
|
|
3676
4117
|
}
|
|
3677
|
-
const _t =
|
|
4118
|
+
const _t = simplifyTypeToSource(type.elementType).replaceAll('\n', '\n' + spaces);
|
|
3678
4119
|
newlineBeforeFirst();
|
|
3679
4120
|
if (templates) {
|
|
3680
|
-
out += `${spaces}if (!inspectTypeWithTemplates(${element.name}, ${_t}, '${
|
|
4121
|
+
out += `${spaces}if (!inspectTypeWithTemplates(${element.name}, ${_t}, '${_loc2}', '${nameFancy}', rtiTemplates)) {\n`;
|
|
3681
4122
|
} else {
|
|
3682
|
-
out += `${spaces}if (!inspectType(${element.name}, ${_t}, '${
|
|
4123
|
+
out += `${spaces}if (!inspectType(${element.name}, ${_t}, '${_loc2}', '${nameFancy}')) {\n`;
|
|
3683
4124
|
}
|
|
3684
4125
|
out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
|
|
3685
4126
|
}
|
|
@@ -3708,12 +4149,12 @@ class Asserter extends Stringifier {
|
|
|
3708
4149
|
this.warn("missing subtype information in JSDoc");
|
|
3709
4150
|
continue;
|
|
3710
4151
|
}
|
|
3711
|
-
const _t2 =
|
|
4152
|
+
const _t2 = simplifyTypeToSource(subType).replaceAll('\n', '\n' + spaces);
|
|
3712
4153
|
newlineBeforeFirst();
|
|
3713
4154
|
if (templates) {
|
|
3714
|
-
out += `${spaces}if (!inspectTypeWithTemplates(${keyName}, ${_t2}, '${
|
|
4155
|
+
out += `${spaces}if (!inspectTypeWithTemplates(${keyName}, ${_t2}, '${_loc2}', '${nameFancy}', rtiTemplates)) {\n`;
|
|
3715
4156
|
} else {
|
|
3716
|
-
out += `${spaces}if (!inspectType(${keyName}, ${_t2}, '${
|
|
4157
|
+
out += `${spaces}if (!inspectType(${keyName}, ${_t2}, '${_loc2}', '${nameFancy}')) {\n`;
|
|
3717
4158
|
}
|
|
3718
4159
|
out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
|
|
3719
4160
|
}
|
|
@@ -3723,17 +4164,17 @@ class Asserter extends Stringifier {
|
|
|
3723
4164
|
name = `arguments[${paramIndex}]`;
|
|
3724
4165
|
}
|
|
3725
4166
|
} else {
|
|
3726
|
-
this.warn(`generateTypeChecks> ${
|
|
4167
|
+
this.warn(`generateTypeChecks> ${_loc2}> todo implement`, `AssignmentPattern for parameter ${name}`);
|
|
3727
4168
|
continue;
|
|
3728
4169
|
}
|
|
3729
4170
|
}
|
|
3730
4171
|
} else {
|
|
3731
|
-
const
|
|
3732
|
-
this.warn(`generateTypeChecks> ${
|
|
4172
|
+
const _loc3 = this.getName(node);
|
|
4173
|
+
this.warn(`generateTypeChecks> ${_loc3}> Missing param: ${name}`);
|
|
3733
4174
|
continue;
|
|
3734
4175
|
}
|
|
3735
4176
|
}
|
|
3736
|
-
let t =
|
|
4177
|
+
let t = simplifyTypeToSource(type).replaceAll('\n', '\n' + spaces);
|
|
3737
4178
|
if (type === 'this') {
|
|
3738
4179
|
const classDecl = this.findParentOfType(node, 'ClassDeclaration');
|
|
3739
4180
|
if (!(classDecl != null && classDecl.id)) {
|
|
@@ -3741,19 +4182,107 @@ class Asserter extends Stringifier {
|
|
|
3741
4182
|
}
|
|
3742
4183
|
t = '"' + this.toSource(classDecl.id) + '"';
|
|
3743
4184
|
}
|
|
3744
|
-
const
|
|
4185
|
+
const _loc4 = this.getName(node);
|
|
3745
4186
|
let prevCheck = '';
|
|
3746
4187
|
// JSDoc doesn't support multiple function signatures yet, but this is
|
|
3747
4188
|
// exactly what we would need to deal with ObjectPool'ing
|
|
3748
|
-
if (
|
|
4189
|
+
if (_loc4 === 'ContactPoint#constructor' || _loc4 === 'ContactResult#constructor' || _loc4 === 'SingleContactResult#constructor') {
|
|
3749
4190
|
prevCheck = 'arguments.length !== 0 && ';
|
|
3750
4191
|
}
|
|
3751
4192
|
newlineBeforeFirst();
|
|
3752
4193
|
if (templates) {
|
|
3753
|
-
out += `${spaces}if (${prevCheck}!inspectTypeWithTemplates(${name}, ${t}, '${
|
|
4194
|
+
out += `${spaces}if (${prevCheck}!inspectTypeWithTemplates(${name}, ${t}, '${_loc4}', '${nameFancy}', rtiTemplates)) {\n`;
|
|
3754
4195
|
} else {
|
|
3755
|
-
out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${
|
|
4196
|
+
out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${_loc4}', '${nameFancy}')) {\n`;
|
|
4197
|
+
}
|
|
4198
|
+
out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
|
|
4199
|
+
}
|
|
4200
|
+
// Params without JSDoc but with inferable defaults get synthesized
|
|
4201
|
+
// optional checks; JSDoc types always win on conflict.
|
|
4202
|
+
out += this.emitDefaultChecks(node, this.collectDefaultChecks(node, new Set(Object.keys(params))), out === '');
|
|
4203
|
+
return out;
|
|
4204
|
+
}
|
|
4205
|
+
/**
|
|
4206
|
+
* Collects type checks for undocumented params: inline
|
|
4207
|
+
* `/** @type *\/` param comments first, default-value inference second.
|
|
4208
|
+
* Only `Identifier` targets missing from `documented` are considered.
|
|
4209
|
+
* @param {Node} node - The Babel AST node for which to generate type checks.
|
|
4210
|
+
* @param {Set<string>} documented - Parameter names covered by JSDoc.
|
|
4211
|
+
* @returns {{name: string, type: any}[]} Checks with optional-annotated types.
|
|
4212
|
+
*/
|
|
4213
|
+
collectDefaultChecks(node, documented) {
|
|
4214
|
+
let fnNode = node;
|
|
4215
|
+
if (fnNode.type === 'BlockStatement') {
|
|
4216
|
+
fnNode = this.parent;
|
|
4217
|
+
}
|
|
4218
|
+
const {
|
|
4219
|
+
params
|
|
4220
|
+
} = fnNode;
|
|
4221
|
+
if (!Array.isArray(params)) {
|
|
4222
|
+
return [];
|
|
4223
|
+
}
|
|
4224
|
+
if (fnNode.type === 'ArrowFunctionExpression' && !this.findParentOfType(fnNode, 'VariableDeclarator')) {
|
|
4225
|
+
// Bare callbacks (e.g. `.forEach((x = 0) => ...)`) can't be named,
|
|
4226
|
+
// synthesizing checks would spam unnameable-callback warnings.
|
|
4227
|
+
return [];
|
|
4228
|
+
}
|
|
4229
|
+
const checks = [];
|
|
4230
|
+
for (const param of params) {
|
|
4231
|
+
var _parseInlineParamType;
|
|
4232
|
+
if (!param) {
|
|
4233
|
+
continue;
|
|
4234
|
+
}
|
|
4235
|
+
const isAssignment = param.type === 'AssignmentPattern';
|
|
4236
|
+
const target = isAssignment ? param.left : param;
|
|
4237
|
+
if (!target || target.type !== 'Identifier' || documented.has(target.name)) {
|
|
4238
|
+
continue;
|
|
3756
4239
|
}
|
|
4240
|
+
// Inline `/** @type *\/` wins over default inference;
|
|
4241
|
+
// a default still marks the check optional.
|
|
4242
|
+
const inline = (_parseInlineParamType = parseInlineParamType(param.leadingComments, this.expandType)) != null ? _parseInlineParamType : parseInlineParamType(target.leadingComments, this.expandType);
|
|
4243
|
+
if (inline !== undefined) {
|
|
4244
|
+
checks.push({
|
|
4245
|
+
name: target.name,
|
|
4246
|
+
type: annotateOptional(inline, isAssignment)
|
|
4247
|
+
});
|
|
4248
|
+
continue;
|
|
4249
|
+
}
|
|
4250
|
+
if (!isAssignment) {
|
|
4251
|
+
continue;
|
|
4252
|
+
}
|
|
4253
|
+
const inferred = inferTypeFromDefault$1(param.right);
|
|
4254
|
+
if (inferred === undefined) {
|
|
4255
|
+
continue;
|
|
4256
|
+
}
|
|
4257
|
+
checks.push({
|
|
4258
|
+
name: target.name,
|
|
4259
|
+
type: annotateOptional(inferred, true)
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
return checks;
|
|
4263
|
+
}
|
|
4264
|
+
/**
|
|
4265
|
+
* Emits code for checks inferred from default values.
|
|
4266
|
+
* @param {Node} node - The Babel AST node for which to generate type checks.
|
|
4267
|
+
* @param {{name: string, type: any}[]} checks - Checks to emit.
|
|
4268
|
+
* @param {boolean} prefixNewline - Separate from preceding code with newline.
|
|
4269
|
+
* @returns {string} A string of code with type check assertions.
|
|
4270
|
+
*/
|
|
4271
|
+
emitDefaultChecks(node, checks, prefixNewline) {
|
|
4272
|
+
if (!checks.length) {
|
|
4273
|
+
return '';
|
|
4274
|
+
}
|
|
4275
|
+
const {
|
|
4276
|
+
spaces
|
|
4277
|
+
} = this;
|
|
4278
|
+
const loc = this.getName(node);
|
|
4279
|
+
let out = prefixNewline ? '\n' : '';
|
|
4280
|
+
for (const {
|
|
4281
|
+
name,
|
|
4282
|
+
type
|
|
4283
|
+
} of checks) {
|
|
4284
|
+
const t = simplifyTypeToSource(type).replaceAll('\n', '\n' + spaces);
|
|
4285
|
+
out += `${spaces}if (!inspectType(${name}, ${t}, '${loc}', '${name}')) {\n`;
|
|
3757
4286
|
out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
|
|
3758
4287
|
}
|
|
3759
4288
|
return out;
|
|
@@ -3783,6 +4312,11 @@ class Asserter extends Stringifier {
|
|
|
3783
4312
|
}
|
|
3784
4313
|
console.warn("Asserter#getNameForFunctionExpression> expression without left");
|
|
3785
4314
|
}
|
|
4315
|
+
const variableDeclarator = this.findParentOfType(node, 'VariableDeclarator');
|
|
4316
|
+
if (variableDeclarator) {
|
|
4317
|
+
// e.g. `var ScopeSpace = function (name) {...}` (issue #82)
|
|
4318
|
+
return this.toSource(variableDeclarator.id);
|
|
4319
|
+
}
|
|
3786
4320
|
return 'unnamed function expression';
|
|
3787
4321
|
}
|
|
3788
4322
|
/**
|
|
@@ -3790,7 +4324,7 @@ class Asserter extends Stringifier {
|
|
|
3790
4324
|
* @returns {string} Stringification of the node.
|
|
3791
4325
|
*/
|
|
3792
4326
|
getName(node) {
|
|
3793
|
-
var _node, _node2;
|
|
4327
|
+
var _node, _node2, _node3;
|
|
3794
4328
|
const toSource = this.toSource.bind(this);
|
|
3795
4329
|
if (node.type === 'BlockStatement') {
|
|
3796
4330
|
node = this.parent;
|
|
@@ -3833,10 +4367,53 @@ class Asserter extends Stringifier {
|
|
|
3833
4367
|
}
|
|
3834
4368
|
// top scope
|
|
3835
4369
|
return `${this.filename}:${(_node = node) == null || (_node = _node.loc) == null || (_node = _node.start) == null ? void 0 : _node.line}`;
|
|
4370
|
+
case 'MemberExpression':
|
|
4371
|
+
if (node.computed) {
|
|
4372
|
+
const memberGoodNames = ['FunctionDeclaration', 'ClassMethod', 'ClassPrivateMethod'];
|
|
4373
|
+
const memberGoodParent = this.parents.findLast(_ => memberGoodNames.includes(_.type));
|
|
4374
|
+
if (memberGoodParent) {
|
|
4375
|
+
return this.getName(memberGoodParent);
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
// top scope
|
|
4379
|
+
return `${this.filename}:${(_node2 = node) == null || (_node2 = _node2.loc) == null || (_node2 = _node2.start) == null ? void 0 : _node2.line}`;
|
|
3836
4380
|
}
|
|
3837
4381
|
this.warn('getName> unhandled type', type, 'for', node, this.path);
|
|
3838
4382
|
//debugger;
|
|
3839
|
-
return `${this.filename}:${(
|
|
4383
|
+
return `${this.filename}:${(_node3 = node) == null || (_node3 = _node3.loc) == null || (_node3 = _node3.start) == null ? void 0 : _node3.line}`;
|
|
4384
|
+
}
|
|
4385
|
+
/**
|
|
4386
|
+
* @override
|
|
4387
|
+
* @param {import("@babel/types").MemberExpression} node - The Babel AST node.
|
|
4388
|
+
* @returns {string} Stringification of the node.
|
|
4389
|
+
*/
|
|
4390
|
+
MemberExpression(node) {
|
|
4391
|
+
const {
|
|
4392
|
+
computed,
|
|
4393
|
+
object,
|
|
4394
|
+
property
|
|
4395
|
+
} = node;
|
|
4396
|
+
if (!computed || object.type === 'Super') {
|
|
4397
|
+
return super.MemberExpression(node);
|
|
4398
|
+
}
|
|
4399
|
+
const parent = this.parent;
|
|
4400
|
+
if (parent) {
|
|
4401
|
+
// `a[i] = ...`, `a[i]++`/`--` and `delete a[i]` are handled by the statement
|
|
4402
|
+
// itself, wrapping them here would produce invalid assignment to a call.
|
|
4403
|
+
if (parent.type === 'AssignmentExpression' && parent.left === node) {
|
|
4404
|
+
return super.MemberExpression(node);
|
|
4405
|
+
}
|
|
4406
|
+
if (parent.type === 'UpdateExpression' && parent.argument === node) {
|
|
4407
|
+
return super.MemberExpression(node);
|
|
4408
|
+
}
|
|
4409
|
+
if (parent.type === 'UnaryExpression' && parent.operator === 'delete' && parent.argument === node) {
|
|
4410
|
+
return super.MemberExpression(node);
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
const object_ = this.toSource(object);
|
|
4414
|
+
const property_ = this.toSource(property);
|
|
4415
|
+
const loc = this.getName(node);
|
|
4416
|
+
return `inspectIndexedAccess(${object_}, ${property_}, ${JSON.stringify(loc)})`;
|
|
3840
4417
|
}
|
|
3841
4418
|
/**
|
|
3842
4419
|
* @override
|
|
@@ -3881,7 +4458,7 @@ class Asserter extends Stringifier {
|
|
|
3881
4458
|
let out = '';
|
|
3882
4459
|
for (const name in this.typedefs) {
|
|
3883
4460
|
const typedef = this.typedefs[name];
|
|
3884
|
-
const json =
|
|
4461
|
+
const json = simplifyTypeToSource(typedef);
|
|
3885
4462
|
out += `registerTypedef('${name}', ${json});\n`;
|
|
3886
4463
|
}
|
|
3887
4464
|
const code = this.toSource(program) + '\n';
|
|
@@ -4066,6 +4643,21 @@ function compareAST(left, right) {
|
|
|
4066
4643
|
* @returns {string|object|undefined} - See `toSourceBabelTS`.
|
|
4067
4644
|
*/
|
|
4068
4645
|
function expandTypeBabelTS(type) {
|
|
4646
|
+
type = type.trim();
|
|
4647
|
+
// JSDocNullableType (`T?` / `?T`): Babel has no support (babel/babel#16073),
|
|
4648
|
+
// handle at string level to match expandType() union-with-null.
|
|
4649
|
+
if (type.endsWith('?') && type.length > 1) {
|
|
4650
|
+
return {
|
|
4651
|
+
type: 'union',
|
|
4652
|
+
members: [expandTypeBabelTS(type.slice(0, -1).trim()), 'null']
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4655
|
+
if (type.startsWith('?') && type.length > 1) {
|
|
4656
|
+
return {
|
|
4657
|
+
type: 'union',
|
|
4658
|
+
members: [expandTypeBabelTS(type.slice(1).trim()), 'null']
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4069
4661
|
const ast = parseTypeBabelTS(type);
|
|
4070
4662
|
return toSourceBabelTS(ast);
|
|
4071
4663
|
}
|
|
@@ -4138,7 +4730,8 @@ function toSourceBabelTS(node) {
|
|
|
4138
4730
|
const name = toSourceBabelTS(node.typeName);
|
|
4139
4731
|
if (!node.typeParameters) {
|
|
4140
4732
|
// console.log(`node.typeName.name=${node.typeName.name} name=${name}`, node);
|
|
4141
|
-
|
|
4733
|
+
// Bare reference: Identifier gives name, TSQualifiedName gives dotted path.
|
|
4734
|
+
return name;
|
|
4142
4735
|
}
|
|
4143
4736
|
console.assert(node.typeParameters.type === 'TSTypeParameterInstantiation');
|
|
4144
4737
|
const typeArguments = node.typeParameters.params;
|
|
@@ -4186,9 +4779,13 @@ function toSourceBabelTS(node) {
|
|
|
4186
4779
|
elementType
|
|
4187
4780
|
};
|
|
4188
4781
|
}
|
|
4189
|
-
|
|
4782
|
+
// Parity with expandType(): generic references like ArrayLike<T>,
|
|
4783
|
+
// ReadonlyArray<T>, NodeListOf<T> or user typedefs like MyBox<T>.
|
|
4784
|
+
const args = typeArguments.map(toSourceBabelTS);
|
|
4190
4785
|
return {
|
|
4191
|
-
type: '
|
|
4786
|
+
type: 'reference',
|
|
4787
|
+
name,
|
|
4788
|
+
args
|
|
4192
4789
|
};
|
|
4193
4790
|
}
|
|
4194
4791
|
case 'TSStringKeyword':
|
|
@@ -4241,7 +4838,7 @@ function toSourceBabelTS(node) {
|
|
|
4241
4838
|
return 'boolean';
|
|
4242
4839
|
// expandTypeBabelTS('true | false');
|
|
4243
4840
|
case 'BooleanLiteral':
|
|
4244
|
-
return node.value
|
|
4841
|
+
return node.value;
|
|
4245
4842
|
// ts.SyntaxKind[parseType("*").kind] === 'JSDocAllType'
|
|
4246
4843
|
// But Babel-TS doesn't parse it atm
|
|
4247
4844
|
//case 'JSDocAllType':
|
|
@@ -4250,6 +4847,7 @@ function toSourceBabelTS(node) {
|
|
|
4250
4847
|
return 'null';
|
|
4251
4848
|
// expandTypeBabelTS('123')
|
|
4252
4849
|
case 'NumericLiteral':
|
|
4850
|
+
return typeof node.value === 'number' ? node.value : Number(node.extra.raw);
|
|
4253
4851
|
case 'StringLiteral':
|
|
4254
4852
|
return node.extra.raw;
|
|
4255
4853
|
// expandTypeBabelTS('undefined')
|
|
@@ -4281,10 +4879,3282 @@ function toSourceBabelTS(node) {
|
|
|
4281
4879
|
type: 'typeof',
|
|
4282
4880
|
argument
|
|
4283
4881
|
};
|
|
4882
|
+
case 'TSTypeOperator':
|
|
4883
|
+
if (node.operator === 'readonly') {
|
|
4884
|
+
// readonly erased at runtime, same shape as the inner type.
|
|
4885
|
+
return toSourceBabelTS(node.typeAnnotation);
|
|
4886
|
+
}
|
|
4887
|
+
console.warn('unimplemented TSTypeOperator', node.operator);
|
|
4888
|
+
return 'any';
|
|
4889
|
+
case 'TSQualifiedName':
|
|
4890
|
+
return `${toSourceBabelTS(node.left)}.${toSourceBabelTS(node.right)}`;
|
|
4284
4891
|
default:
|
|
4285
4892
|
console.warn('toSourceBabelTS> unhandled type', node.type, node);
|
|
4286
4893
|
debugger;
|
|
4287
4894
|
}
|
|
4288
4895
|
}
|
|
4289
4896
|
|
|
4290
|
-
|
|
4897
|
+
/**
|
|
4898
|
+
* Map of Babel node types to their child keys that contain traversable AST nodes.
|
|
4899
|
+
* @type {Record<string, string[]>}
|
|
4900
|
+
*/
|
|
4901
|
+
const nodeChildren = {
|
|
4902
|
+
'ArrayExpression': ['elements'],
|
|
4903
|
+
'ArrayPattern': ['elements'],
|
|
4904
|
+
'ArrowFunctionExpression': ['params', 'body'],
|
|
4905
|
+
'AssignmentExpression': ['left', 'right'],
|
|
4906
|
+
'AssignmentPattern': ['left', 'right'],
|
|
4907
|
+
'AwaitExpression': ['argument'],
|
|
4908
|
+
'BinaryExpression': ['left', 'right'],
|
|
4909
|
+
'BlockStatement': ['directives', 'body'],
|
|
4910
|
+
'BreakStatement': ['label'],
|
|
4911
|
+
'CallExpression': ['callee', 'arguments'],
|
|
4912
|
+
'CatchClause': ['param', 'body'],
|
|
4913
|
+
'ClassBody': ['body'],
|
|
4914
|
+
'ClassDeclaration': ['id', 'superClass', 'body'],
|
|
4915
|
+
'ClassExpression': ['id', 'superClass', 'body'],
|
|
4916
|
+
'ClassMethod': ['key', 'params', 'body'],
|
|
4917
|
+
'ClassPrivateMethod': ['key', 'params', 'body'],
|
|
4918
|
+
'ClassPrivateProperty': ['key', 'value'],
|
|
4919
|
+
'ClassProperty': ['key', 'value'],
|
|
4920
|
+
'ConditionalExpression': ['test', 'consequent', 'alternate'],
|
|
4921
|
+
'ContinueStatement': ['label'],
|
|
4922
|
+
'DebuggerStatement': [],
|
|
4923
|
+
'Directive': ['value'],
|
|
4924
|
+
'DirectiveLiteral': [],
|
|
4925
|
+
'DoWhileStatement': ['body', 'test'],
|
|
4926
|
+
'EmptyStatement': [],
|
|
4927
|
+
'ExportAllDeclaration': ['source'],
|
|
4928
|
+
'ExportDefaultDeclaration': ['declaration'],
|
|
4929
|
+
'ExportNamedDeclaration': ['declaration', 'specifiers', 'source'],
|
|
4930
|
+
'ExportNamespaceSpecifier': ['exported'],
|
|
4931
|
+
'ExportSpecifier': ['local', 'exported'],
|
|
4932
|
+
'ExpressionStatement': ['expression'],
|
|
4933
|
+
'File': ['program'],
|
|
4934
|
+
'ForInStatement': ['left', 'right', 'body'],
|
|
4935
|
+
'ForOfStatement': ['left', 'right', 'body'],
|
|
4936
|
+
'ForStatement': ['init', 'test', 'update', 'body'],
|
|
4937
|
+
'FunctionDeclaration': ['id', 'params', 'body'],
|
|
4938
|
+
'FunctionExpression': ['id', 'params', 'body'],
|
|
4939
|
+
'IfStatement': ['test', 'consequent', 'alternate'],
|
|
4940
|
+
'Import': [],
|
|
4941
|
+
'ImportDeclaration': ['specifiers', 'source'],
|
|
4942
|
+
'ImportDefaultSpecifier': ['local'],
|
|
4943
|
+
'ImportExpression': ['source'],
|
|
4944
|
+
'ImportNamespaceSpecifier': ['local'],
|
|
4945
|
+
'ImportSpecifier': ['imported', 'local'],
|
|
4946
|
+
'JSXAttribute': ['name', 'value'],
|
|
4947
|
+
'JSXElement': ['openingElement', 'children', 'closingElement'],
|
|
4948
|
+
'JSXExpressionContainer': ['expression'],
|
|
4949
|
+
'JSXFragment': ['openingFragment', 'children', 'closingFragment'],
|
|
4950
|
+
'JSXIdentifier': [],
|
|
4951
|
+
'JSXMemberExpression': ['object', 'property'],
|
|
4952
|
+
'JSXNamespacedName': ['namespace', 'name'],
|
|
4953
|
+
'JSXText': [],
|
|
4954
|
+
'LabeledStatement': ['label', 'body'],
|
|
4955
|
+
'LogicalExpression': ['left', 'right'],
|
|
4956
|
+
'MemberExpression': ['object', 'property'],
|
|
4957
|
+
'MetaProperty': ['meta', 'property'],
|
|
4958
|
+
'NewExpression': ['callee', 'arguments'],
|
|
4959
|
+
'ObjectExpression': ['properties'],
|
|
4960
|
+
'ObjectMethod': ['key', 'params', 'body'],
|
|
4961
|
+
'ObjectPattern': ['properties'],
|
|
4962
|
+
'ObjectProperty': ['key', 'value'],
|
|
4963
|
+
'OptionalCallExpression': ['callee', 'arguments'],
|
|
4964
|
+
'OptionalMemberExpression': ['object', 'property'],
|
|
4965
|
+
'ParenthesizedExpression': ['expression'],
|
|
4966
|
+
'PrivateName': ['id'],
|
|
4967
|
+
'Program': ['directives', 'body'],
|
|
4968
|
+
'RegExpLiteral': [],
|
|
4969
|
+
'RestElement': ['argument'],
|
|
4970
|
+
'ReturnStatement': ['argument'],
|
|
4971
|
+
'SequenceExpression': ['expressions'],
|
|
4972
|
+
'SpreadElement': ['argument'],
|
|
4973
|
+
'Super': [],
|
|
4974
|
+
'SwitchCase': ['test', 'consequent'],
|
|
4975
|
+
'SwitchStatement': ['discriminant', 'cases'],
|
|
4976
|
+
'TaggedTemplateExpression': ['tag', 'quasi'],
|
|
4977
|
+
'TemplateElement': [],
|
|
4978
|
+
'TemplateLiteral': ['quasis', 'expressions'],
|
|
4979
|
+
'ThisExpression': [],
|
|
4980
|
+
'ThrowStatement': ['argument'],
|
|
4981
|
+
'TryStatement': ['block', 'handler', 'finalizer'],
|
|
4982
|
+
'UnaryExpression': ['argument'],
|
|
4983
|
+
'UpdateExpression': ['argument'],
|
|
4984
|
+
'VariableDeclaration': ['declarations'],
|
|
4985
|
+
'VariableDeclarator': ['id', 'init'],
|
|
4986
|
+
'WhileStatement': ['test', 'body'],
|
|
4987
|
+
'YieldExpression': ['argument'],
|
|
4988
|
+
'TSAsExpression': ['expression', 'typeAnnotation'],
|
|
4989
|
+
'TSDeclareFunction': ['id', 'params', 'body'],
|
|
4990
|
+
'TSEnumDeclaration': ['id', 'members'],
|
|
4991
|
+
'TSEnumMember': ['id', 'initializer'],
|
|
4992
|
+
'TSInterfaceDeclaration': ['id', 'body'],
|
|
4993
|
+
'TSInterfaceBody': ['body'],
|
|
4994
|
+
'TSModuleDeclaration': ['id', 'body'],
|
|
4995
|
+
'TSModuleBlock': ['body'],
|
|
4996
|
+
'TSNonNullExpression': ['expression'],
|
|
4997
|
+
'TSParameterProperty': ['parameter'],
|
|
4998
|
+
'TSInstantiationExpression': ['expression'],
|
|
4999
|
+
'TSTypeAliasDeclaration': ['id', 'typeAnnotation'],
|
|
5000
|
+
'TSTypeAssertion': ['expression'],
|
|
5001
|
+
'TSTypeCastExpression': ['expression'],
|
|
5002
|
+
'TSTypeAnnotation': ['typeAnnotation'],
|
|
5003
|
+
'TSTypeParameterDeclaration': ['params'],
|
|
5004
|
+
'TSTypeParameterInstantiation': ['params']
|
|
5005
|
+
};
|
|
5006
|
+
|
|
5007
|
+
class JSDocAnnotator {
|
|
5008
|
+
/**
|
|
5009
|
+
* @param {Object} [options] - Options for the annotator.
|
|
5010
|
+
* @param {import('./parseJSDoc.js').ExpandType} [options.expandType] - Function to expand types.
|
|
5011
|
+
*/
|
|
5012
|
+
constructor(options = {}) {
|
|
5013
|
+
/** @type {import('@babel/types').Node[]} */
|
|
5014
|
+
this.parents = [];
|
|
5015
|
+
/** @type {Record<string, object>} */
|
|
5016
|
+
this.typedefs = {};
|
|
5017
|
+
/** @type {import('./parseJSDoc.js').ExpandType} */
|
|
5018
|
+
this.expandType = options.expandType || expandTypeDepFree;
|
|
5019
|
+
}
|
|
5020
|
+
/**
|
|
5021
|
+
* Annotates the AST by adding 'jsdoc' properties to relevant nodes.
|
|
5022
|
+
* @param {import('@babel/types').File|import('@babel/types').Program} ast - The Babel AST to annotate.
|
|
5023
|
+
* @returns {import('@babel/types').File|import('@babel/types').Program} The annotated AST.
|
|
5024
|
+
*/
|
|
5025
|
+
annotate(ast) {
|
|
5026
|
+
if (ast.type === 'File') {
|
|
5027
|
+
const {
|
|
5028
|
+
comments
|
|
5029
|
+
} = ast;
|
|
5030
|
+
if (comments) {
|
|
5031
|
+
for (const comment of comments) {
|
|
5032
|
+
const warn = console.warn.bind(console);
|
|
5033
|
+
parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
this.traverse(ast.program);
|
|
5037
|
+
ast.program.typedefs = this.typedefs;
|
|
5038
|
+
} else {
|
|
5039
|
+
this.traverse(ast);
|
|
5040
|
+
ast.typedefs = this.typedefs;
|
|
5041
|
+
}
|
|
5042
|
+
return ast;
|
|
5043
|
+
}
|
|
5044
|
+
/**
|
|
5045
|
+
* Traverses the AST node and annotates where applicable.
|
|
5046
|
+
* @param {import('@babel/types').Node} node - The node to traverse.
|
|
5047
|
+
*/
|
|
5048
|
+
traverse(node) {
|
|
5049
|
+
if (!node) return;
|
|
5050
|
+
this.parents.push(node);
|
|
5051
|
+
if (nodeIsFunctionLike(node)) {
|
|
5052
|
+
const jsdoc = this.getJSDoc(node);
|
|
5053
|
+
if (jsdoc) {
|
|
5054
|
+
node.jsdoc = jsdoc;
|
|
5055
|
+
const paramTypes = this.collectParamTypes(node);
|
|
5056
|
+
if (Object.keys(paramTypes).length > 0) {
|
|
5057
|
+
node.paramTypes = paramTypes;
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
const type = node.type;
|
|
5062
|
+
const childrenKeys = nodeChildren[type] || [];
|
|
5063
|
+
for (const key of childrenKeys) {
|
|
5064
|
+
const child = node[key];
|
|
5065
|
+
if (Array.isArray(child)) {
|
|
5066
|
+
for (const c of child) {
|
|
5067
|
+
this.traverse(c);
|
|
5068
|
+
}
|
|
5069
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
5070
|
+
this.traverse(child);
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
this.parents.pop();
|
|
5074
|
+
}
|
|
5075
|
+
/**
|
|
5076
|
+
* Collects the parameter names into a map of names to types.
|
|
5077
|
+
* @param {import('@babel/types').ArrowFunctionExpression | import('@babel/types').FunctionDeclaration | import('@babel/types').FunctionExpression | import('@babel/types').ObjectMethod | import('@babel/types').ClassMethod | import('@babel/types').ClassPrivateMethod} node - The function-like node.
|
|
5078
|
+
* @returns {Record<string, string | object>} The map of parameter names to types.
|
|
5079
|
+
*/
|
|
5080
|
+
collectParamTypes(node) {
|
|
5081
|
+
var _node$jsdoc;
|
|
5082
|
+
const jsdocParams = ((_node$jsdoc = node.jsdoc) == null ? void 0 : _node$jsdoc.params) || {};
|
|
5083
|
+
const paramNames = Object.keys(jsdocParams);
|
|
5084
|
+
const paramTypes = {};
|
|
5085
|
+
node.params.forEach((paramNode, index) => {
|
|
5086
|
+
const paramName = paramNames[index];
|
|
5087
|
+
if (paramName) {
|
|
5088
|
+
const typeInfo = jsdocParams[paramName];
|
|
5089
|
+
this.collectParamNames(paramNode, typeInfo, paramTypes);
|
|
5090
|
+
}
|
|
5091
|
+
});
|
|
5092
|
+
return paramTypes;
|
|
5093
|
+
}
|
|
5094
|
+
/**
|
|
5095
|
+
* Recursively collects parameter names from a pattern with their types.
|
|
5096
|
+
* @param {import('@babel/types').PatternLike} paramNode - The parameter node.
|
|
5097
|
+
* @param {string | object} typeInfo - The type information.
|
|
5098
|
+
* @param {Record<string, string | object>} map - The map to collect into.
|
|
5099
|
+
*/
|
|
5100
|
+
collectParamNames(paramNode, typeInfo, map) {
|
|
5101
|
+
if (!typeInfo) {
|
|
5102
|
+
console.warn("!typeInfo", {
|
|
5103
|
+
paramNode,
|
|
5104
|
+
typeInfo
|
|
5105
|
+
});
|
|
5106
|
+
return;
|
|
5107
|
+
}
|
|
5108
|
+
const {
|
|
5109
|
+
type
|
|
5110
|
+
} = paramNode;
|
|
5111
|
+
if (type === 'Identifier') {
|
|
5112
|
+
map[paramNode.name] = typeInfo;
|
|
5113
|
+
} else if (type === 'ObjectPattern') {
|
|
5114
|
+
if (typeof typeInfo !== 'object' || typeInfo.type !== 'object' || !typeInfo.properties) {
|
|
5115
|
+
console.warn("typeof typeInfo !== 'object' || typeInfo.type !== 'object' || !typeInfo.properties", {
|
|
5116
|
+
paramNode,
|
|
5117
|
+
typeInfo
|
|
5118
|
+
});
|
|
5119
|
+
return;
|
|
5120
|
+
}
|
|
5121
|
+
const propTypes = typeInfo.properties;
|
|
5122
|
+
paramNode.properties.forEach(prop => {
|
|
5123
|
+
if (prop.type !== 'ObjectProperty') {
|
|
5124
|
+
console.warn("prop.type !== 'ObjectProperty'", {
|
|
5125
|
+
paramNode,
|
|
5126
|
+
typeInfo
|
|
5127
|
+
});
|
|
5128
|
+
return;
|
|
5129
|
+
}
|
|
5130
|
+
if (prop.key.type !== 'Identifier') {
|
|
5131
|
+
console.warn("prop.key.type !== 'Identifier'", {
|
|
5132
|
+
paramNode,
|
|
5133
|
+
typeInfo
|
|
5134
|
+
});
|
|
5135
|
+
return;
|
|
5136
|
+
}
|
|
5137
|
+
const keyName = prop.key.name;
|
|
5138
|
+
const subType = propTypes[keyName];
|
|
5139
|
+
if (!subType) {
|
|
5140
|
+
console.warn("!subType", {
|
|
5141
|
+
paramNode,
|
|
5142
|
+
typeInfo
|
|
5143
|
+
});
|
|
5144
|
+
return;
|
|
5145
|
+
}
|
|
5146
|
+
this.collectParamNames(prop.value, subType, map);
|
|
5147
|
+
});
|
|
5148
|
+
} else if (type === 'ArrayPattern') {
|
|
5149
|
+
// console.log("ARRAY PATTERN", {type, typeInfo});
|
|
5150
|
+
if (typeof typeInfo !== 'object') {
|
|
5151
|
+
console.warn("typeof typeInfo !== 'object'", {
|
|
5152
|
+
paramNode,
|
|
5153
|
+
typeInfo
|
|
5154
|
+
});
|
|
5155
|
+
return;
|
|
5156
|
+
}
|
|
5157
|
+
switch (typeInfo.type) {
|
|
5158
|
+
case 'array':
|
|
5159
|
+
if (!typeInfo.elementType) {
|
|
5160
|
+
console.warn("Expected array type, but missing 'elementType' property.", {
|
|
5161
|
+
paramNode,
|
|
5162
|
+
typeInfo
|
|
5163
|
+
});
|
|
5164
|
+
return;
|
|
5165
|
+
}
|
|
5166
|
+
const elementType = typeInfo.elementType;
|
|
5167
|
+
paramNode.elements.forEach(el => {
|
|
5168
|
+
if (!el) {
|
|
5169
|
+
console.warn("!el", {
|
|
5170
|
+
paramNode,
|
|
5171
|
+
typeInfo
|
|
5172
|
+
});
|
|
5173
|
+
return;
|
|
5174
|
+
}
|
|
5175
|
+
this.collectParamNames(el, elementType, map);
|
|
5176
|
+
});
|
|
5177
|
+
break;
|
|
5178
|
+
case 'tuple':
|
|
5179
|
+
// console.log("TUPLE PATTERN", {type, typeInfo});
|
|
5180
|
+
if (!typeInfo.elements) {
|
|
5181
|
+
console.warn("Expected tuple type, but missing 'elements' property.", {
|
|
5182
|
+
paramNode,
|
|
5183
|
+
typeInfo
|
|
5184
|
+
});
|
|
5185
|
+
return;
|
|
5186
|
+
}
|
|
5187
|
+
const elements = typeInfo.elements;
|
|
5188
|
+
paramNode.elements.forEach((el, i) => {
|
|
5189
|
+
const elType = elements[i];
|
|
5190
|
+
if (el === null) {
|
|
5191
|
+
// Example missing 'd' param: function addStr(a, {b}, [c], [, e])
|
|
5192
|
+
return;
|
|
5193
|
+
}
|
|
5194
|
+
// console.log(`paramNode.elements[${i}]`, el, "elType", elType);
|
|
5195
|
+
this.collectParamNames(el, elType, map);
|
|
5196
|
+
});
|
|
5197
|
+
break;
|
|
5198
|
+
default:
|
|
5199
|
+
console.warn('Unsupported type for ArrayPattern:', typeInfo.type, {
|
|
5200
|
+
paramNode,
|
|
5201
|
+
typeInfo
|
|
5202
|
+
});
|
|
5203
|
+
break;
|
|
5204
|
+
}
|
|
5205
|
+
} else if (type === 'AssignmentPattern') {
|
|
5206
|
+
this.collectParamNames(paramNode.left, typeInfo, map);
|
|
5207
|
+
} else if (type === 'RestElement') {
|
|
5208
|
+
this.collectParamNames(paramNode.argument, typeInfo, map);
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
/**
|
|
5212
|
+
* Finds the closest ancestor of the given node that matches the specified type.
|
|
5213
|
+
* @param {import('@babel/types').Node} node - The starting node.
|
|
5214
|
+
* @param {string} type - The type to search for.
|
|
5215
|
+
* @returns {import('@babel/types').Node|undefined} The ancestor node or undefined.
|
|
5216
|
+
*/
|
|
5217
|
+
findParentOfType(node, type) {
|
|
5218
|
+
const currentIndex = this.parents.findLastIndex(_ => _ === node);
|
|
5219
|
+
return this.parents.findLast((_, i) => i <= currentIndex && _.type === type);
|
|
5220
|
+
}
|
|
5221
|
+
/**
|
|
5222
|
+
* Gets the leading comment node for ArrowFunctionExpression.
|
|
5223
|
+
* @param {import('@babel/types').Node} node - The node.
|
|
5224
|
+
* @returns {import('@babel/types').Node|undefined} The node with leading comments.
|
|
5225
|
+
*/
|
|
5226
|
+
getLeadingCommentsNodeForArrowFunctionExpression(node) {
|
|
5227
|
+
let i = this.parents.findLastIndex(_ => _ === node);
|
|
5228
|
+
let parent = this.parents[i];
|
|
5229
|
+
if (parent.leadingComments) {
|
|
5230
|
+
return parent;
|
|
5231
|
+
}
|
|
5232
|
+
i--;
|
|
5233
|
+
while (i >= 0) {
|
|
5234
|
+
parent = this.parents[i];
|
|
5235
|
+
if (nodeIsFunctionLike(parent)) {
|
|
5236
|
+
break;
|
|
5237
|
+
}
|
|
5238
|
+
if (parent.leadingComments) {
|
|
5239
|
+
return parent;
|
|
5240
|
+
}
|
|
5241
|
+
i--;
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
/**
|
|
5245
|
+
* Gets the leading comment node for FunctionExpression.
|
|
5246
|
+
* @param {import('@babel/types').Node} node - The node.
|
|
5247
|
+
* @returns {import('@babel/types').Node|undefined} The node with leading comments.
|
|
5248
|
+
*/
|
|
5249
|
+
getLeadingCommentsNodeForFunctionExpression(node) {
|
|
5250
|
+
let i = this.parents.findLastIndex(_ => _ === node);
|
|
5251
|
+
let parent = this.parents[i];
|
|
5252
|
+
if (parent.leadingComments) {
|
|
5253
|
+
return parent;
|
|
5254
|
+
}
|
|
5255
|
+
i--;
|
|
5256
|
+
while (i >= 0) {
|
|
5257
|
+
parent = this.parents[i];
|
|
5258
|
+
if (nodeIsFunctionLike(parent)) {
|
|
5259
|
+
break;
|
|
5260
|
+
}
|
|
5261
|
+
if (parent.leadingComments) {
|
|
5262
|
+
return parent;
|
|
5263
|
+
}
|
|
5264
|
+
i--;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
/**
|
|
5268
|
+
* Gets the leading comment string for a node.
|
|
5269
|
+
* @param {import('@babel/types').Node} node - The node.
|
|
5270
|
+
* @returns {string|undefined} The comment value.
|
|
5271
|
+
*/
|
|
5272
|
+
getLeadingComment(node) {
|
|
5273
|
+
let leadingComments = node.leadingComments;
|
|
5274
|
+
if (!leadingComments) {
|
|
5275
|
+
if (node.type === 'FunctionDeclaration') {
|
|
5276
|
+
const exportNamedDeclaration = this.findParentOfType(node, 'ExportNamedDeclaration');
|
|
5277
|
+
leadingComments = exportNamedDeclaration == null ? void 0 : exportNamedDeclaration.leadingComments;
|
|
5278
|
+
} else if (node.type === 'ArrowFunctionExpression') {
|
|
5279
|
+
const tmp = this.getLeadingCommentsNodeForArrowFunctionExpression(node);
|
|
5280
|
+
leadingComments = tmp == null ? void 0 : tmp.leadingComments;
|
|
5281
|
+
} else if (node.type === 'FunctionExpression') {
|
|
5282
|
+
const tmp = this.getLeadingCommentsNodeForFunctionExpression(node);
|
|
5283
|
+
leadingComments = tmp == null ? void 0 : tmp.leadingComments;
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
if (leadingComments && leadingComments.length) {
|
|
5287
|
+
const lastComment = leadingComments[leadingComments.length - 1];
|
|
5288
|
+
if (lastComment.type === 'CommentBlock') {
|
|
5289
|
+
return lastComment.value;
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5293
|
+
/**
|
|
5294
|
+
* Parses the JSDoc for a node.
|
|
5295
|
+
* @param {import('@babel/types').Node} node - The node.
|
|
5296
|
+
* @returns {{templates: any, params: any}|undefined} The parsed JSDoc.
|
|
5297
|
+
*/
|
|
5298
|
+
getJSDoc(node) {
|
|
5299
|
+
const comment = this.getLeadingComment(node);
|
|
5300
|
+
if (!comment) {
|
|
5301
|
+
return;
|
|
5302
|
+
}
|
|
5303
|
+
if (comment.includes('@event') || comment.includes('@ignoreRTI')) {
|
|
5304
|
+
return;
|
|
5305
|
+
}
|
|
5306
|
+
if (node.type === 'ClassMethod' && node.kind === 'set') {
|
|
5307
|
+
if (node.params.length !== 1) {
|
|
5308
|
+
console.warn('getJSDoc> setters require exactly one argument');
|
|
5309
|
+
}
|
|
5310
|
+
const setterType = parseJSDocSetter(comment, this.expandType);
|
|
5311
|
+
if (!setterType) {
|
|
5312
|
+
return;
|
|
5313
|
+
}
|
|
5314
|
+
const paramName = node.params[0].type === 'Identifier' ? node.params[0].name : 'value';
|
|
5315
|
+
const _params = {
|
|
5316
|
+
[paramName]: setterType
|
|
5317
|
+
};
|
|
5318
|
+
return {
|
|
5319
|
+
templates: undefined,
|
|
5320
|
+
params: _params
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
const templates = parseJSDocTemplates(comment);
|
|
5324
|
+
const params = parseJSDoc(comment, this.expandType);
|
|
5325
|
+
if (!templates && !params) {
|
|
5326
|
+
return;
|
|
5327
|
+
}
|
|
5328
|
+
return {
|
|
5329
|
+
templates,
|
|
5330
|
+
params
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
|
|
5335
|
+
/** @typedef {import("@babel/types").Node} Node */
|
|
5336
|
+
const CONST_TYPE = {
|
|
5337
|
+
f32: 'f32.const',
|
|
5338
|
+
f64: 'f64.const',
|
|
5339
|
+
i32: 'i32.const',
|
|
5340
|
+
i64: 'i64.const'
|
|
5341
|
+
};
|
|
5342
|
+
const NEG_OP = {
|
|
5343
|
+
f32: 'f32.neg',
|
|
5344
|
+
f64: 'f64.neg'
|
|
5345
|
+
};
|
|
5346
|
+
const EQZ_OP = {
|
|
5347
|
+
f32: 'f32.eqz',
|
|
5348
|
+
f64: 'f64.eqz',
|
|
5349
|
+
i32: 'i32.eqz',
|
|
5350
|
+
i64: 'i64.eqz'
|
|
5351
|
+
};
|
|
5352
|
+
const NE_OP = {
|
|
5353
|
+
f32: 'f32.ne',
|
|
5354
|
+
f64: 'f64.ne',
|
|
5355
|
+
i32: 'i32.ne',
|
|
5356
|
+
i64: 'i64.ne'
|
|
5357
|
+
};
|
|
5358
|
+
const ARITH_OP = {
|
|
5359
|
+
f32: {
|
|
5360
|
+
add: 'f32.add',
|
|
5361
|
+
sub: 'f32.sub',
|
|
5362
|
+
mul: 'f32.mul',
|
|
5363
|
+
div: 'f32.div'
|
|
5364
|
+
},
|
|
5365
|
+
f64: {
|
|
5366
|
+
add: 'f64.add',
|
|
5367
|
+
sub: 'f64.sub',
|
|
5368
|
+
mul: 'f64.mul',
|
|
5369
|
+
div: 'f64.div'
|
|
5370
|
+
},
|
|
5371
|
+
i32: {
|
|
5372
|
+
add: 'i32.add',
|
|
5373
|
+
sub: 'i32.sub',
|
|
5374
|
+
mul: 'i32.mul',
|
|
5375
|
+
div: 'i32.div_s'
|
|
5376
|
+
},
|
|
5377
|
+
i64: {
|
|
5378
|
+
add: 'i64.add',
|
|
5379
|
+
sub: 'i64.sub',
|
|
5380
|
+
mul: 'i64.mul',
|
|
5381
|
+
div: 'i64.div_s'
|
|
5382
|
+
}
|
|
5383
|
+
};
|
|
5384
|
+
const CMP_OP = {
|
|
5385
|
+
f32: {
|
|
5386
|
+
lt: 'f32.lt',
|
|
5387
|
+
gt: 'f32.gt',
|
|
5388
|
+
le: 'f32.le',
|
|
5389
|
+
ge: 'f32.ge',
|
|
5390
|
+
eq: 'f32.eq',
|
|
5391
|
+
ne: 'f32.ne'
|
|
5392
|
+
},
|
|
5393
|
+
f64: {
|
|
5394
|
+
lt: 'f64.lt',
|
|
5395
|
+
gt: 'f64.gt',
|
|
5396
|
+
le: 'f64.le',
|
|
5397
|
+
ge: 'f64.ge',
|
|
5398
|
+
eq: 'f64.eq',
|
|
5399
|
+
ne: 'f64.ne'
|
|
5400
|
+
},
|
|
5401
|
+
i32: {
|
|
5402
|
+
lt: 'i32.lt_s',
|
|
5403
|
+
gt: 'i32.gt_s',
|
|
5404
|
+
le: 'i32.le_s',
|
|
5405
|
+
ge: 'i32.ge_s',
|
|
5406
|
+
eq: 'i32.eq',
|
|
5407
|
+
ne: 'i32.ne'
|
|
5408
|
+
},
|
|
5409
|
+
i64: {
|
|
5410
|
+
lt: 'i64.lt_s',
|
|
5411
|
+
gt: 'i64.gt_s',
|
|
5412
|
+
le: 'i64.le_s',
|
|
5413
|
+
ge: 'i64.ge_s',
|
|
5414
|
+
eq: 'i64.eq',
|
|
5415
|
+
ne: 'i64.ne'
|
|
5416
|
+
}
|
|
5417
|
+
};
|
|
5418
|
+
const MATH_OP = {
|
|
5419
|
+
f32: {
|
|
5420
|
+
abs: 'f32.abs',
|
|
5421
|
+
sqrt: 'f32.sqrt',
|
|
5422
|
+
min: 'f32.min',
|
|
5423
|
+
max: 'f32.max',
|
|
5424
|
+
floor: 'f32.floor',
|
|
5425
|
+
ceil: 'f32.ceil',
|
|
5426
|
+
trunc: 'f32.trunc'
|
|
5427
|
+
},
|
|
5428
|
+
f64: {
|
|
5429
|
+
abs: 'f64.abs',
|
|
5430
|
+
sqrt: 'f64.sqrt',
|
|
5431
|
+
min: 'f64.min',
|
|
5432
|
+
max: 'f64.max',
|
|
5433
|
+
floor: 'f64.floor',
|
|
5434
|
+
ceil: 'f64.ceil',
|
|
5435
|
+
trunc: 'f64.trunc'
|
|
5436
|
+
}
|
|
5437
|
+
};
|
|
5438
|
+
const CMP_OPERATORS = ['<', '<=', '>', '>=', '==', '!='];
|
|
5439
|
+
/**
|
|
5440
|
+
* Class for converting a JavaScript AST into WebAssembly Text (WAT).
|
|
5441
|
+
*/
|
|
5442
|
+
class WATConverter extends Stringifier {
|
|
5443
|
+
constructor(...args) {
|
|
5444
|
+
super(...args);
|
|
5445
|
+
/**
|
|
5446
|
+
* Current numeric WAT type for emitted operations.
|
|
5447
|
+
* @type {'f32'|'f64'|'i32'|'i64'}
|
|
5448
|
+
*/
|
|
5449
|
+
this.currentType = 'f32';
|
|
5450
|
+
/**
|
|
5451
|
+
* Stack of enclosing loop labels used as break/continue targets.
|
|
5452
|
+
* @type {Array<{exit: string, top: string, updateSrc: string}>}
|
|
5453
|
+
*/
|
|
5454
|
+
this.loops = [];
|
|
5455
|
+
/**
|
|
5456
|
+
* Counter for unique loop label names.
|
|
5457
|
+
* @type {number}
|
|
5458
|
+
*/
|
|
5459
|
+
this.loopCounter = 0;
|
|
5460
|
+
/**
|
|
5461
|
+
* Linear-memory array allocations by variable name.
|
|
5462
|
+
* @type {Object<string, {offset: number, values: number[], isObject: boolean, keys?: Map<string, number>}>}
|
|
5463
|
+
*/
|
|
5464
|
+
this.arrays = {};
|
|
5465
|
+
/**
|
|
5466
|
+
* Declared classes by name, each with its instance fields, per-field byte offsets, methods and total size.
|
|
5467
|
+
* @type {Object<string, {fields: string[], offsets: Object<string, number>, defaults: Object<string, Node>, methods: Set<string>, size: number}>}
|
|
5468
|
+
*/
|
|
5469
|
+
this.classes = {};
|
|
5470
|
+
/**
|
|
5471
|
+
* Instance variable names mapped to their class names.
|
|
5472
|
+
* @type {Object<string, string>}
|
|
5473
|
+
*/
|
|
5474
|
+
this.instances = {};
|
|
5475
|
+
/**
|
|
5476
|
+
* Name of the class currently being converted, if any.
|
|
5477
|
+
* @type {string|null}
|
|
5478
|
+
*/
|
|
5479
|
+
this.currentClass = null;
|
|
5480
|
+
}
|
|
5481
|
+
/**
|
|
5482
|
+
* Converts a Babel AST node to WAT source.
|
|
5483
|
+
* @param {Node} node - The Babel AST node.
|
|
5484
|
+
* @returns {string} WAT representation of the node.
|
|
5485
|
+
*/
|
|
5486
|
+
toSource(node) {
|
|
5487
|
+
if (node === null) {
|
|
5488
|
+
return '';
|
|
5489
|
+
}
|
|
5490
|
+
const {
|
|
5491
|
+
leadingComments,
|
|
5492
|
+
trailingComments
|
|
5493
|
+
} = node;
|
|
5494
|
+
if (node.type === 'File') {
|
|
5495
|
+
this.parents.length = 0;
|
|
5496
|
+
}
|
|
5497
|
+
this.parents.push(node);
|
|
5498
|
+
let out = '';
|
|
5499
|
+
if (leadingComments) {
|
|
5500
|
+
out += this.leadingCommentsToSource(leadingComments);
|
|
5501
|
+
}
|
|
5502
|
+
out += this.toSource_(node);
|
|
5503
|
+
if (trailingComments) {
|
|
5504
|
+
out += this.trailingCommentsToSource(trailingComments);
|
|
5505
|
+
}
|
|
5506
|
+
this.parents.pop();
|
|
5507
|
+
return out;
|
|
5508
|
+
}
|
|
5509
|
+
/**
|
|
5510
|
+
* Dispatches a node to its handler method, emitting a warning comment for unhandled types.
|
|
5511
|
+
* @param {Node} node - The Babel AST node.
|
|
5512
|
+
* @returns {string} WAT representation of the node.
|
|
5513
|
+
*/
|
|
5514
|
+
toSource_(node) {
|
|
5515
|
+
if (!node) {
|
|
5516
|
+
return '';
|
|
5517
|
+
}
|
|
5518
|
+
const {
|
|
5519
|
+
type
|
|
5520
|
+
} = node;
|
|
5521
|
+
if (this[type]) {
|
|
5522
|
+
return this[type](node);
|
|
5523
|
+
}
|
|
5524
|
+
console.warn(`TODO ADD METHOD: ${type}`);
|
|
5525
|
+
return `;; Unhandled node type: ${type}\n`;
|
|
5526
|
+
}
|
|
5527
|
+
/**
|
|
5528
|
+
* Converts leading comments to WAT.
|
|
5529
|
+
* @param {import("@babel/types").Comment[]} comments - The comment nodes.
|
|
5530
|
+
* @returns {string} WAT representation of comments.
|
|
5531
|
+
*/
|
|
5532
|
+
leadingCommentsToSource(comments) {
|
|
5533
|
+
return comments.map(comment => this.commentToSource(comment, 'leading')).join('');
|
|
5534
|
+
}
|
|
5535
|
+
/**
|
|
5536
|
+
* Converts trailing comments to WAT.
|
|
5537
|
+
* @param {import("@babel/types").Comment[]} comments - The comment nodes.
|
|
5538
|
+
* @returns {string} WAT representation of comments.
|
|
5539
|
+
*/
|
|
5540
|
+
trailingCommentsToSource(comments) {
|
|
5541
|
+
return comments.map(comment => this.commentToSource(comment, 'trailing')).join('');
|
|
5542
|
+
}
|
|
5543
|
+
/**
|
|
5544
|
+
* Converts a comment to WAT.
|
|
5545
|
+
* @param {import("@babel/types").Comment} comment - The comment node.
|
|
5546
|
+
* @param {'leading' | 'trailing'} pos - The position of the comment.
|
|
5547
|
+
* @returns {string} WAT representation of the comment.
|
|
5548
|
+
*/
|
|
5549
|
+
commentToSource(comment, pos) {
|
|
5550
|
+
if (comment.type === 'CommentBlock') {
|
|
5551
|
+
return this.CommentBlock(comment);
|
|
5552
|
+
} else if (comment.type === 'CommentLine') {
|
|
5553
|
+
return this.CommentLine(comment);
|
|
5554
|
+
}
|
|
5555
|
+
console.warn("Unknown comment type", comment);
|
|
5556
|
+
return '';
|
|
5557
|
+
}
|
|
5558
|
+
/**
|
|
5559
|
+
* Converts a CommentBlock to WAT.
|
|
5560
|
+
* @param {import("@babel/types").CommentBlock} node - The comment node.
|
|
5561
|
+
* @returns {string} WAT representation of the comment.
|
|
5562
|
+
*/
|
|
5563
|
+
CommentBlock(node) {
|
|
5564
|
+
const {
|
|
5565
|
+
loc,
|
|
5566
|
+
value
|
|
5567
|
+
} = node;
|
|
5568
|
+
if (this.lastCommentBlockIndex === loc.start.index) {
|
|
5569
|
+
return '';
|
|
5570
|
+
}
|
|
5571
|
+
this.lastCommentBlockIndex = loc.start.index;
|
|
5572
|
+
const spaces = this.spaces;
|
|
5573
|
+
let out = '';
|
|
5574
|
+
const dedicatedLine = spaces.length === loc.start.column;
|
|
5575
|
+
if (dedicatedLine) {
|
|
5576
|
+
out += spaces;
|
|
5577
|
+
}
|
|
5578
|
+
const multiLine = loc.start.line !== loc.end.line;
|
|
5579
|
+
if (multiLine) {
|
|
5580
|
+
out += '\n' + spaces;
|
|
5581
|
+
}
|
|
5582
|
+
out += ';;' + value.replace(/\n\s*\*/g, '\n' + spaces + ';;') + '\n';
|
|
5583
|
+
if (!dedicatedLine) {
|
|
5584
|
+
out += ' ';
|
|
5585
|
+
}
|
|
5586
|
+
return out;
|
|
5587
|
+
}
|
|
5588
|
+
/**
|
|
5589
|
+
* Converts a CommentLine to WAT.
|
|
5590
|
+
* @param {import("@babel/types").CommentLine} node - The comment node.
|
|
5591
|
+
* @returns {string} WAT representation of the comment.
|
|
5592
|
+
*/
|
|
5593
|
+
CommentLine(node) {
|
|
5594
|
+
const {
|
|
5595
|
+
value,
|
|
5596
|
+
loc
|
|
5597
|
+
} = node;
|
|
5598
|
+
if (this.lastCommentLineIndex === loc.start.index) {
|
|
5599
|
+
return '';
|
|
5600
|
+
}
|
|
5601
|
+
this.lastCommentLineIndex = loc.start.index;
|
|
5602
|
+
const spaces = this.spaces;
|
|
5603
|
+
const dedicatedLine = spaces.length === loc.start.column;
|
|
5604
|
+
let out = dedicatedLine ? '\n' + spaces : ' ';
|
|
5605
|
+
out += `;;${value}\n`;
|
|
5606
|
+
return out;
|
|
5607
|
+
}
|
|
5608
|
+
/**
|
|
5609
|
+
* Runs a function with a temporarily shifted indentation depth.
|
|
5610
|
+
* @param {number} extra - Additional indentation depth.
|
|
5611
|
+
* @param {() => string} fn - The function to run.
|
|
5612
|
+
* @returns {string} The result of the function.
|
|
5613
|
+
*/
|
|
5614
|
+
atDepth(extra, fn) {
|
|
5615
|
+
this.numSpaces += extra;
|
|
5616
|
+
const ret = fn();
|
|
5617
|
+
this.numSpaces -= extra;
|
|
5618
|
+
return ret;
|
|
5619
|
+
}
|
|
5620
|
+
/**
|
|
5621
|
+
* Converts a Program node to WAT.
|
|
5622
|
+
* @param {import("@babel/types").Program} node - The Babel AST node.
|
|
5623
|
+
* @returns {string} WAT representation of the node.
|
|
5624
|
+
*/
|
|
5625
|
+
Program(node) {
|
|
5626
|
+
const {
|
|
5627
|
+
body
|
|
5628
|
+
} = node;
|
|
5629
|
+
this.registerModuleData(body);
|
|
5630
|
+
this.registerClasses(body);
|
|
5631
|
+
const classNames = Object.keys(this.classes);
|
|
5632
|
+
const {
|
|
5633
|
+
spaces
|
|
5634
|
+
} = this;
|
|
5635
|
+
let out = `(module\n`;
|
|
5636
|
+
this.numSpaces++;
|
|
5637
|
+
const rest = body.filter(stmt => this.isModuleDataDeclaration(stmt) === false);
|
|
5638
|
+
out += this.mapToSource(rest).join('');
|
|
5639
|
+
const names = Object.keys(this.arrays);
|
|
5640
|
+
if (classNames.length) {
|
|
5641
|
+
out += `${this.spaces}(memory (export "m") 512)\n`;
|
|
5642
|
+
names.forEach(name => {
|
|
5643
|
+
const arr = this.arrays[name];
|
|
5644
|
+
out += `${this.spaces}(data (i32.const ${arr.offset}) ${this.f32DataBytes(arr.values)})\n`;
|
|
5645
|
+
});
|
|
5646
|
+
out += this.allocatorSource();
|
|
5647
|
+
} else if (names.length) {
|
|
5648
|
+
out += `${this.spaces}(memory (export "m") 1)\n`;
|
|
5649
|
+
names.forEach(name => {
|
|
5650
|
+
const arr = this.arrays[name];
|
|
5651
|
+
out += `${this.spaces}(data (i32.const ${arr.offset}) ${this.f32DataBytes(arr.values)})\n`;
|
|
5652
|
+
});
|
|
5653
|
+
}
|
|
5654
|
+
this.numSpaces--;
|
|
5655
|
+
out += `${spaces})\n`;
|
|
5656
|
+
return out;
|
|
5657
|
+
}
|
|
5658
|
+
/**
|
|
5659
|
+
* Registers top-level array/object literals as flat memory data.
|
|
5660
|
+
* @param {import("@babel/types").Statement[]} body - Top-level statements.
|
|
5661
|
+
*/
|
|
5662
|
+
registerModuleData(body) {
|
|
5663
|
+
let offset = 0;
|
|
5664
|
+
body.forEach(stmt => {
|
|
5665
|
+
if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') {
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
const decl = stmt.declarations[0];
|
|
5669
|
+
if (!decl || decl.id.type !== 'Identifier') {
|
|
5670
|
+
return;
|
|
5671
|
+
}
|
|
5672
|
+
const {
|
|
5673
|
+
init
|
|
5674
|
+
} = decl;
|
|
5675
|
+
if (!init) {
|
|
5676
|
+
return;
|
|
5677
|
+
}
|
|
5678
|
+
if (init.type === 'ArrayExpression') {
|
|
5679
|
+
const values = init.elements.map(e => (e == null ? void 0 : e.type) === 'NumericLiteral' ? e.value : 0.0);
|
|
5680
|
+
this.arrays[decl.id.name] = {
|
|
5681
|
+
offset,
|
|
5682
|
+
values,
|
|
5683
|
+
isObject: false
|
|
5684
|
+
};
|
|
5685
|
+
offset += values.length * 4;
|
|
5686
|
+
} else if (init.type === 'ObjectExpression') {
|
|
5687
|
+
/** @type {Map<string, number>} */
|
|
5688
|
+
const keys = new Map();
|
|
5689
|
+
/** @type {number[]} */
|
|
5690
|
+
const values = [];
|
|
5691
|
+
init.properties.forEach((prop, i) => {
|
|
5692
|
+
if (prop.type !== 'ObjectProperty' || prop.key.type !== 'Identifier' || prop.value.type !== 'NumericLiteral') {
|
|
5693
|
+
return;
|
|
5694
|
+
}
|
|
5695
|
+
keys.set(prop.key.name, i);
|
|
5696
|
+
values.push(prop.value.value);
|
|
5697
|
+
});
|
|
5698
|
+
this.arrays[decl.id.name] = {
|
|
5699
|
+
offset,
|
|
5700
|
+
values,
|
|
5701
|
+
isObject: true,
|
|
5702
|
+
keys
|
|
5703
|
+
};
|
|
5704
|
+
offset += values.length * 4;
|
|
5705
|
+
}
|
|
5706
|
+
});
|
|
5707
|
+
}
|
|
5708
|
+
/**
|
|
5709
|
+
* Checks whether a statement is a module-level array/object constant declaration.
|
|
5710
|
+
* @param {import("@babel/types").Statement} stmt - The statement.
|
|
5711
|
+
* @returns {boolean} True if the statement is a module-level data declaration.
|
|
5712
|
+
*/
|
|
5713
|
+
isModuleDataDeclaration(stmt) {
|
|
5714
|
+
if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') {
|
|
5715
|
+
return false;
|
|
5716
|
+
}
|
|
5717
|
+
const decl = stmt.declarations[0];
|
|
5718
|
+
return !!decl && decl.id.type === 'Identifier' && !!this.arrays[decl.id.name];
|
|
5719
|
+
}
|
|
5720
|
+
/**
|
|
5721
|
+
* Serializes the f32 values of an array into raw little-endian bytes for a data segment.
|
|
5722
|
+
* @param {number[]} values - The f32 values.
|
|
5723
|
+
* @returns {string} The escaped byte string.
|
|
5724
|
+
*/
|
|
5725
|
+
f32DataBytes(values) {
|
|
5726
|
+
const bytes = new Uint8Array(new Float32Array(values).buffer);
|
|
5727
|
+
let out = '"';
|
|
5728
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
5729
|
+
out += '\\' + bytes[i].toString(16).padStart(2, '0');
|
|
5730
|
+
}
|
|
5731
|
+
return out + '"';
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Registers top-level class declarations and their instance memory layout.
|
|
5735
|
+
* @param {import("@babel/types").Statement[]} body - Top-level statements.
|
|
5736
|
+
*/
|
|
5737
|
+
registerClasses(body) {
|
|
5738
|
+
body.forEach(stmt => {
|
|
5739
|
+
let decl = stmt;
|
|
5740
|
+
if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {
|
|
5741
|
+
decl = stmt.declaration;
|
|
5742
|
+
}
|
|
5743
|
+
if (decl.type === 'ClassDeclaration') {
|
|
5744
|
+
this.registerClass(decl);
|
|
5745
|
+
}
|
|
5746
|
+
});
|
|
5747
|
+
}
|
|
5748
|
+
/**
|
|
5749
|
+
* Scans one class for instance fields, defaults and methods and stores its memory layout.
|
|
5750
|
+
* @param {import("@babel/types").ClassDeclaration} node - The class declaration.
|
|
5751
|
+
*/
|
|
5752
|
+
registerClass(node) {
|
|
5753
|
+
const {
|
|
5754
|
+
id,
|
|
5755
|
+
body
|
|
5756
|
+
} = node;
|
|
5757
|
+
const cls = {
|
|
5758
|
+
fields: [],
|
|
5759
|
+
offsets: {},
|
|
5760
|
+
defaults: {},
|
|
5761
|
+
methods: new Set(),
|
|
5762
|
+
size: 0
|
|
5763
|
+
};
|
|
5764
|
+
body.body.forEach(member => {
|
|
5765
|
+
if (member.static) {
|
|
5766
|
+
return;
|
|
5767
|
+
}
|
|
5768
|
+
if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
|
|
5769
|
+
const name = member.key.type === 'Identifier' ? member.key.name : member.key.value;
|
|
5770
|
+
if (member.kind === 'constructor') {
|
|
5771
|
+
var _member$value;
|
|
5772
|
+
const methodBody = ((_member$value = member.value) == null ? void 0 : _member$value.body) || member.body;
|
|
5773
|
+
if (methodBody) {
|
|
5774
|
+
this.collectConstructorFields(methodBody, cls);
|
|
5775
|
+
}
|
|
5776
|
+
} else {
|
|
5777
|
+
cls.methods.add(name);
|
|
5778
|
+
}
|
|
5779
|
+
} else if (member.type === 'ClassProperty' || member.type === 'PropertyDefinition') {
|
|
5780
|
+
const name = member.key.type === 'Identifier' ? member.key.name : member.key.value;
|
|
5781
|
+
this.addClassField(cls, name);
|
|
5782
|
+
if (member.value) {
|
|
5783
|
+
cls.defaults[name] = member.value;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
});
|
|
5787
|
+
if (cls.size || cls.methods.size) {
|
|
5788
|
+
this.classes[id.name] = cls;
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
/**
|
|
5792
|
+
* Adds a class field at the next available byte offset.
|
|
5793
|
+
* @param {Object} cls - The class layout.
|
|
5794
|
+
* @param {string} name - The field name.
|
|
5795
|
+
*/
|
|
5796
|
+
addClassField(cls, name) {
|
|
5797
|
+
if (name in cls.offsets) {
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
const index = cls.fields.length;
|
|
5801
|
+
cls.offsets[name] = index * 4;
|
|
5802
|
+
cls.fields.push(name);
|
|
5803
|
+
cls.size = (index + 1) * 4;
|
|
5804
|
+
}
|
|
5805
|
+
/**
|
|
5806
|
+
* Collects class fields assigned in a constructor from `this.name = ...` stores.
|
|
5807
|
+
* @param {import("@babel/types").BlockStatement} body - The constructor body.
|
|
5808
|
+
* @param {Object} cls - The class layout.
|
|
5809
|
+
*/
|
|
5810
|
+
collectConstructorFields(body, cls) {
|
|
5811
|
+
this.walk(body, node => {
|
|
5812
|
+
const target = node.type === 'AssignmentExpression' ? node.left : node.type === 'UpdateExpression' ? node.argument : null;
|
|
5813
|
+
if (!target || target.type !== 'MemberExpression') {
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
const isThis = target.object.type === 'ThisExpression' || target.object.type === 'Identifier' && target.object.name === 'this';
|
|
5817
|
+
if (!isThis) {
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
const name = target.property.type === 'Identifier' ? target.property.name : target.property.value;
|
|
5821
|
+
this.addClassField(cls, name);
|
|
5822
|
+
});
|
|
5823
|
+
}
|
|
5824
|
+
/**
|
|
5825
|
+
* Walks a Babel node tree, invoking a callback for every node.
|
|
5826
|
+
* @param {Node} node - The node to walk.
|
|
5827
|
+
* @param {(node: Node) => void} fn - The callback.
|
|
5828
|
+
*/
|
|
5829
|
+
walk(node, fn) {
|
|
5830
|
+
if (!node) {
|
|
5831
|
+
return;
|
|
5832
|
+
}
|
|
5833
|
+
fn(node);
|
|
5834
|
+
for (const key of Object.keys(node)) {
|
|
5835
|
+
if (key === 'leadingComments' || key === 'trailingComments' || key === 'loc' || key === 'start' || key === 'end') {
|
|
5836
|
+
continue;
|
|
5837
|
+
}
|
|
5838
|
+
const child = node[key];
|
|
5839
|
+
if (Array.isArray(child)) {
|
|
5840
|
+
child.forEach(c => this.walk(c, fn));
|
|
5841
|
+
} else if (child && typeof child.type === 'string') {
|
|
5842
|
+
this.walk(child, fn);
|
|
5843
|
+
}
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
/**
|
|
5847
|
+
* Computes the first unused byte, after all module-level array/object data.
|
|
5848
|
+
* @returns {number} The heap start offset.
|
|
5849
|
+
*/
|
|
5850
|
+
heapStart() {
|
|
5851
|
+
let max = 0;
|
|
5852
|
+
Object.values(this.arrays).forEach(arr => {
|
|
5853
|
+
const end = arr.offset + arr.values.length * 4;
|
|
5854
|
+
if (end > max) {
|
|
5855
|
+
max = end;
|
|
5856
|
+
}
|
|
5857
|
+
});
|
|
5858
|
+
return max;
|
|
5859
|
+
}
|
|
5860
|
+
/**
|
|
5861
|
+
* Emits the bump allocator: a heap pointer global and an `$alloc` function.
|
|
5862
|
+
* @returns {string} WAT source of the heap global and allocator.
|
|
5863
|
+
*/
|
|
5864
|
+
allocatorSource() {
|
|
5865
|
+
const {
|
|
5866
|
+
spaces
|
|
5867
|
+
} = this;
|
|
5868
|
+
let out = `${spaces}(global $heapPtr (mut i32) (i32.const ${this.heapStart()}))\n`;
|
|
5869
|
+
out += `${spaces}(func $alloc (param $size i32) (result i32)\n`;
|
|
5870
|
+
this.numSpaces++;
|
|
5871
|
+
out += `${this.spaces}(local $result i32)\n`;
|
|
5872
|
+
out += `${this.spaces}(local.set $result (global.get $heapPtr))\n`;
|
|
5873
|
+
out += `${this.spaces}(global.set $heapPtr (i32.add (global.get $heapPtr) (local.get $size)))\n`;
|
|
5874
|
+
out += `${this.spaces}(local.get $result)\n`;
|
|
5875
|
+
this.numSpaces--;
|
|
5876
|
+
out += `${spaces})\n`;
|
|
5877
|
+
return out;
|
|
5878
|
+
}
|
|
5879
|
+
/**
|
|
5880
|
+
* Converts an ExportNamedDeclaration node to WAT.
|
|
5881
|
+
* @param {import("@babel/types").ExportNamedDeclaration} node - The Babel AST node.
|
|
5882
|
+
* @returns {string} WAT representation of the node.
|
|
5883
|
+
*/
|
|
5884
|
+
ExportNamedDeclaration(node) {
|
|
5885
|
+
const {
|
|
5886
|
+
declaration
|
|
5887
|
+
} = node;
|
|
5888
|
+
let out = '';
|
|
5889
|
+
if (declaration && declaration.type === 'FunctionDeclaration') {
|
|
5890
|
+
out += this.toSource(declaration);
|
|
5891
|
+
const funcName = declaration.id.name;
|
|
5892
|
+
out += `${this.spaces}(export "${funcName}" (func $${funcName}))\n`;
|
|
5893
|
+
} else if (declaration && declaration.type === 'ClassDeclaration') {
|
|
5894
|
+
out += this.toSource(declaration);
|
|
5895
|
+
} else {
|
|
5896
|
+
out += `;; Unsupported ExportNamedDeclaration: ${JSON.stringify(node)}\n`;
|
|
5897
|
+
}
|
|
5898
|
+
return out;
|
|
5899
|
+
}
|
|
5900
|
+
/**
|
|
5901
|
+
* Extracts the numeric WAT type from JSDoc comments (e.g. `@param {i32} n`).
|
|
5902
|
+
* Looks at the function itself and its ancestors, since Babel often attaches
|
|
5903
|
+
* the comment to the wrapping ExportNamedDeclaration instead.
|
|
5904
|
+
* @param {import("@babel/types").FunctionDeclaration} node - The function declaration.
|
|
5905
|
+
* @returns {'f32'|'f64'|'i32'|'i64'} The numeric type.
|
|
5906
|
+
*/
|
|
5907
|
+
getFuncNumericType(node) {
|
|
5908
|
+
let comments = node.leadingComments || [];
|
|
5909
|
+
if (!comments.length) {
|
|
5910
|
+
for (const parent of this.parents) {
|
|
5911
|
+
var _parent$leadingCommen;
|
|
5912
|
+
if ((_parent$leadingCommen = parent.leadingComments) != null && _parent$leadingCommen.length) {
|
|
5913
|
+
comments = parent.leadingComments;
|
|
5914
|
+
break;
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
const text = comments.map(c => c.value).join('\n');
|
|
5919
|
+
const regex = /@(?:param|returns)\s*\{([^}]+)\}/g;
|
|
5920
|
+
let match;
|
|
5921
|
+
while ((match = regex.exec(text)) !== null) {
|
|
5922
|
+
const found = this.jsTypeToWat(match[1].trim().toLowerCase());
|
|
5923
|
+
if (found) {
|
|
5924
|
+
return found;
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
return 'f32';
|
|
5928
|
+
}
|
|
5929
|
+
/**
|
|
5930
|
+
* Maps a JSDoc type name to a WAT numeric type.
|
|
5931
|
+
* @param {string} type - The JSDoc type name.
|
|
5932
|
+
* @returns {'f32'|'f64'|'i32'|'i64'|null} The WAT type or null if not numeric.
|
|
5933
|
+
*/
|
|
5934
|
+
jsTypeToWat(type) {
|
|
5935
|
+
if (type === 'i32' || type === 'int' || type === 'integer') {
|
|
5936
|
+
return 'i32';
|
|
5937
|
+
}
|
|
5938
|
+
if (type === 'i64' || type === 'long') {
|
|
5939
|
+
return 'i64';
|
|
5940
|
+
}
|
|
5941
|
+
if (type === 'f64' || type === 'double') {
|
|
5942
|
+
return 'f64';
|
|
5943
|
+
}
|
|
5944
|
+
if (type === 'f32' || type === 'float' || type === 'number') {
|
|
5945
|
+
return 'f32';
|
|
5946
|
+
}
|
|
5947
|
+
return null;
|
|
5948
|
+
}
|
|
5949
|
+
/**
|
|
5950
|
+
* Converts a FunctionDeclaration node to WAT.
|
|
5951
|
+
* @param {import("@babel/types").FunctionDeclaration} node - The Babel AST node.
|
|
5952
|
+
* @returns {string} WAT representation of the node.
|
|
5953
|
+
*/
|
|
5954
|
+
FunctionDeclaration(node) {
|
|
5955
|
+
const {
|
|
5956
|
+
id,
|
|
5957
|
+
params,
|
|
5958
|
+
body
|
|
5959
|
+
} = node;
|
|
5960
|
+
const funcName = id.name;
|
|
5961
|
+
const savedType = this.currentType;
|
|
5962
|
+
this.currentType = this.getFuncNumericType(node);
|
|
5963
|
+
const t = this.currentType;
|
|
5964
|
+
const paramList = params.map(param => `(param $${param.name} ${t})`).join(' ');
|
|
5965
|
+
const returnType = `(result ${t})`;
|
|
5966
|
+
let out = `${this.spaces}(func $${funcName}${paramList ? ' ' + paramList : ''} ${returnType}\n`;
|
|
5967
|
+
this.numSpaces++;
|
|
5968
|
+
const localNames = this.collectLocalNames(body);
|
|
5969
|
+
localNames.forEach(localName => {
|
|
5970
|
+
out += `${this.spaces}(local $${localName} ${t})\n`;
|
|
5971
|
+
});
|
|
5972
|
+
this.parents.push({
|
|
5973
|
+
type: 'BlockStatement'
|
|
5974
|
+
});
|
|
5975
|
+
out += `${this.spaces}(block $exit (result ${t})\n`;
|
|
5976
|
+
this.numSpaces++;
|
|
5977
|
+
out += body.body.map(statement => this.toSource(statement)).join('');
|
|
5978
|
+
this.numSpaces--;
|
|
5979
|
+
out += `${this.spaces})\n`;
|
|
5980
|
+
this.parents.pop();
|
|
5981
|
+
this.numSpaces--;
|
|
5982
|
+
out += `${this.spaces})\n`;
|
|
5983
|
+
this.currentType = savedType;
|
|
5984
|
+
return out;
|
|
5985
|
+
}
|
|
5986
|
+
/**
|
|
5987
|
+
* Converts a class constructor to a `$<Name>_new` factory that allocates a slot
|
|
5988
|
+
* in the bump heap and returns the instance pointer (as f32).
|
|
5989
|
+
* @param {import("@babel/types").ClassDeclaration} node - The class declaration.
|
|
5990
|
+
* @param {Object} cls - The registered class layout.
|
|
5991
|
+
* @returns {string} WAT source of the factory function.
|
|
5992
|
+
*/
|
|
5993
|
+
classConstructorSource(node, cls) {
|
|
5994
|
+
var _ctor$value;
|
|
5995
|
+
const {
|
|
5996
|
+
spaces
|
|
5997
|
+
} = this;
|
|
5998
|
+
const {
|
|
5999
|
+
id,
|
|
6000
|
+
body
|
|
6001
|
+
} = node;
|
|
6002
|
+
const clsName = id.name;
|
|
6003
|
+
const ctor = body.body.find(m => (m.type === 'ClassMethod' || m.type === 'MethodDefinition') && m.kind === 'constructor' && !m.static);
|
|
6004
|
+
const savedType = this.currentType;
|
|
6005
|
+
this.currentType = 'f32';
|
|
6006
|
+
const savedClass = this.currentClass;
|
|
6007
|
+
this.currentClass = clsName;
|
|
6008
|
+
const ctorParams = ctor ? ((_ctor$value = ctor.value) == null ? void 0 : _ctor$value.params) || ctor.params : [];
|
|
6009
|
+
const paramList = ctorParams.map(p => `(param $${p.name} f32)`).join(' ');
|
|
6010
|
+
let out = `${spaces}(func $${clsName}_new${paramList ? ' ' + paramList : ''} (result f32)\n`;
|
|
6011
|
+
this.numSpaces++;
|
|
6012
|
+
out += `${this.spaces}(local $this f32)\n`;
|
|
6013
|
+
if (ctor) {
|
|
6014
|
+
var _ctor$value2;
|
|
6015
|
+
const ctorBody = ((_ctor$value2 = ctor.value) == null ? void 0 : _ctor$value2.body) || ctor.body;
|
|
6016
|
+
const localNames = this.collectLocalNames(ctorBody);
|
|
6017
|
+
localNames.forEach(name => {
|
|
6018
|
+
out += `${this.spaces}(local $${name} f32)\n`;
|
|
6019
|
+
});
|
|
6020
|
+
}
|
|
6021
|
+
out += `${this.spaces}(local.set $this\n`;
|
|
6022
|
+
this.numSpaces++;
|
|
6023
|
+
out += `${this.spaces}(f32.convert_i32_u\n`;
|
|
6024
|
+
this.numSpaces++;
|
|
6025
|
+
out += `${this.spaces}(call $alloc\n`;
|
|
6026
|
+
this.numSpaces++;
|
|
6027
|
+
out += `${this.spaces}(i32.const ${cls.size})\n`;
|
|
6028
|
+
this.numSpaces--;
|
|
6029
|
+
out += `${this.spaces})\n`;
|
|
6030
|
+
this.numSpaces--;
|
|
6031
|
+
out += `${this.spaces})\n`;
|
|
6032
|
+
this.numSpaces--;
|
|
6033
|
+
out += `${this.spaces})\n`;
|
|
6034
|
+
cls.fields.forEach(name => {
|
|
6035
|
+
if (cls.defaults[name]) {
|
|
6036
|
+
out += this.classFieldStoreSource('this', cls.defaults[name], name, cls);
|
|
6037
|
+
}
|
|
6038
|
+
});
|
|
6039
|
+
if (ctor) {
|
|
6040
|
+
var _ctor$value3;
|
|
6041
|
+
const ctorBody = ((_ctor$value3 = ctor.value) == null ? void 0 : _ctor$value3.body) || ctor.body;
|
|
6042
|
+
out += this.mapToSource(ctorBody.body).join('');
|
|
6043
|
+
}
|
|
6044
|
+
out += `${this.spaces}(local.get $this)\n`;
|
|
6045
|
+
this.numSpaces--;
|
|
6046
|
+
out += `${spaces})\n`;
|
|
6047
|
+
this.currentType = savedType;
|
|
6048
|
+
this.currentClass = savedClass;
|
|
6049
|
+
return out;
|
|
6050
|
+
}
|
|
6051
|
+
/**
|
|
6052
|
+
* Converts a class method to a `$<Name>_<method>` function taking `$this` first.
|
|
6053
|
+
* @param {string} clsName - The class name.
|
|
6054
|
+
* @param {import("@babel/types").ClassMethod} method - The method definition.
|
|
6055
|
+
* @returns {string} WAT source of the method function.
|
|
6056
|
+
*/
|
|
6057
|
+
classMethodSource(clsName, method) {
|
|
6058
|
+
var _method$value, _method$value2;
|
|
6059
|
+
const {
|
|
6060
|
+
spaces
|
|
6061
|
+
} = this;
|
|
6062
|
+
const methodName = method.key.type === 'Identifier' ? method.key.name : method.key.value;
|
|
6063
|
+
const fnParams = ((_method$value = method.value) == null ? void 0 : _method$value.params) || method.params;
|
|
6064
|
+
const fnBody = ((_method$value2 = method.value) == null ? void 0 : _method$value2.body) || method.body;
|
|
6065
|
+
const savedType = this.currentType;
|
|
6066
|
+
this.currentType = 'f32';
|
|
6067
|
+
const paramNames = ['this', ...fnParams.map(p => p.name)];
|
|
6068
|
+
const paramList = paramNames.map(name => `(param $${name} f32)`).join(' ');
|
|
6069
|
+
let out = `${spaces}(func $${clsName}_${methodName} ${paramList} (result f32)\n`;
|
|
6070
|
+
this.numSpaces++;
|
|
6071
|
+
const localNames = this.collectLocalNames(fnBody);
|
|
6072
|
+
localNames.forEach(name => {
|
|
6073
|
+
out += `${this.spaces}(local $${name} f32)\n`;
|
|
6074
|
+
});
|
|
6075
|
+
out += `${this.spaces}(block $exit (result f32)\n`;
|
|
6076
|
+
this.numSpaces++;
|
|
6077
|
+
out += this.mapToSource(fnBody.body).join('');
|
|
6078
|
+
const last = fnBody.body[fnBody.body.length - 1];
|
|
6079
|
+
if (!(last && last.type === 'ReturnStatement')) {
|
|
6080
|
+
out += `${this.spaces}(f32.const 0)\n`;
|
|
6081
|
+
}
|
|
6082
|
+
this.numSpaces--;
|
|
6083
|
+
out += `${this.spaces})\n`;
|
|
6084
|
+
this.numSpaces--;
|
|
6085
|
+
out += `${this.spaces})\n`;
|
|
6086
|
+
this.currentType = savedType;
|
|
6087
|
+
return out;
|
|
6088
|
+
}
|
|
6089
|
+
/**
|
|
6090
|
+
* Converts a ClassDeclaration node to its factory and method functions.
|
|
6091
|
+
* @param {import("@babel/types").ClassDeclaration} node - The class declaration.
|
|
6092
|
+
* @returns {string} WAT representation of the class.
|
|
6093
|
+
*/
|
|
6094
|
+
ClassDeclaration(node) {
|
|
6095
|
+
const clsName = node.id.name;
|
|
6096
|
+
const cls = this.classes[clsName];
|
|
6097
|
+
const {
|
|
6098
|
+
spaces
|
|
6099
|
+
} = this;
|
|
6100
|
+
if (!cls) {
|
|
6101
|
+
return `${spaces};; Unsupported class declaration: ${JSON.stringify(node)}\n`;
|
|
6102
|
+
}
|
|
6103
|
+
let out = this.classConstructorSource(node, cls);
|
|
6104
|
+
node.body.body.forEach(member => {
|
|
6105
|
+
if (member.type !== 'ClassMethod' && member.type !== 'MethodDefinition' || member.kind === 'constructor' || member.static) {
|
|
6106
|
+
return;
|
|
6107
|
+
}
|
|
6108
|
+
const savedClass = this.currentClass;
|
|
6109
|
+
this.currentClass = clsName;
|
|
6110
|
+
out += this.classMethodSource(clsName, member);
|
|
6111
|
+
this.currentClass = savedClass;
|
|
6112
|
+
});
|
|
6113
|
+
return out;
|
|
6114
|
+
}
|
|
6115
|
+
/**
|
|
6116
|
+
* Collects the names of all variables declared inside a function body.
|
|
6117
|
+
* @param {import("@babel/types").BlockStatement} body - The function body.
|
|
6118
|
+
* @returns {string[]} Names of the declared locals.
|
|
6119
|
+
*/
|
|
6120
|
+
collectLocalNames(body) {
|
|
6121
|
+
/** @type {string[]} */
|
|
6122
|
+
const localNames = [];
|
|
6123
|
+
const visit = node => {
|
|
6124
|
+
if (!node) {
|
|
6125
|
+
return;
|
|
6126
|
+
}
|
|
6127
|
+
const {
|
|
6128
|
+
type
|
|
6129
|
+
} = node;
|
|
6130
|
+
if (type === 'VariableDeclaration') {
|
|
6131
|
+
node.declarations.forEach(decl => {
|
|
6132
|
+
if (decl.id.type === 'Identifier' && !localNames.includes(decl.id.name)) {
|
|
6133
|
+
localNames.push(decl.id.name);
|
|
6134
|
+
}
|
|
6135
|
+
});
|
|
6136
|
+
return;
|
|
6137
|
+
}
|
|
6138
|
+
for (const key of Object.keys(node)) {
|
|
6139
|
+
if (key === 'leadingComments' || key === 'trailingComments' || key === 'loc' || key === 'start' || key === 'end') {
|
|
6140
|
+
continue;
|
|
6141
|
+
}
|
|
6142
|
+
const child = node[key];
|
|
6143
|
+
if (Array.isArray(child)) {
|
|
6144
|
+
child.forEach(visit);
|
|
6145
|
+
} else if (child && typeof child.type === 'string') {
|
|
6146
|
+
visit(child);
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
};
|
|
6150
|
+
visit(body);
|
|
6151
|
+
return localNames;
|
|
6152
|
+
}
|
|
6153
|
+
/**
|
|
6154
|
+
* Converts a BlockStatement node to WAT.
|
|
6155
|
+
* @param {import("@babel/types").BlockStatement} node - The Babel AST node.
|
|
6156
|
+
* @returns {string} WAT representation of the node.
|
|
6157
|
+
*/
|
|
6158
|
+
BlockStatement(node) {
|
|
6159
|
+
const {
|
|
6160
|
+
body
|
|
6161
|
+
} = node;
|
|
6162
|
+
return this.mapToSource(body).join('');
|
|
6163
|
+
}
|
|
6164
|
+
/**
|
|
6165
|
+
* Checks whether an expression already produces an i32 boolean test result,
|
|
6166
|
+
* so it can be fed to `if`/`br_if` without a truthiness conversion.
|
|
6167
|
+
* @param {Node} expr - The expression.
|
|
6168
|
+
* @returns {boolean} True if the expression yields an i32 test result.
|
|
6169
|
+
*/
|
|
6170
|
+
isBooleanI32(expr) {
|
|
6171
|
+
if (expr.type === 'BinaryExpression') {
|
|
6172
|
+
return CMP_OPERATORS.includes(expr.operator);
|
|
6173
|
+
}
|
|
6174
|
+
if (expr.type === 'UnaryExpression' && expr.operator === '!') {
|
|
6175
|
+
return true;
|
|
6176
|
+
}
|
|
6177
|
+
if (expr.type === 'LogicalExpression') {
|
|
6178
|
+
return this.isBooleanI32(expr.left) && this.isBooleanI32(expr.right);
|
|
6179
|
+
}
|
|
6180
|
+
return false;
|
|
6181
|
+
}
|
|
6182
|
+
/**
|
|
6183
|
+
* Produces a WAT i32 test expression for an arbitrary numeric condition.
|
|
6184
|
+
* @param {Node} expr - The condition expression.
|
|
6185
|
+
* @returns {string} WAT source that pushes an i32.
|
|
6186
|
+
*/
|
|
6187
|
+
getTestSource(expr) {
|
|
6188
|
+
if (this.isBooleanI32(expr)) {
|
|
6189
|
+
return this.toSource(expr);
|
|
6190
|
+
}
|
|
6191
|
+
const {
|
|
6192
|
+
spaces
|
|
6193
|
+
} = this;
|
|
6194
|
+
const t = this.currentType;
|
|
6195
|
+
let out = `${spaces}(${NE_OP[t]}\n`;
|
|
6196
|
+
this.numSpaces++;
|
|
6197
|
+
out += this.toSource(expr);
|
|
6198
|
+
out += `${this.spaces}(${CONST_TYPE[t]} 0)\n`;
|
|
6199
|
+
this.numSpaces--;
|
|
6200
|
+
out += `${spaces})\n`;
|
|
6201
|
+
return out;
|
|
6202
|
+
}
|
|
6203
|
+
/**
|
|
6204
|
+
* Converts a loop/conditional body, accepting either a block or a single statement.
|
|
6205
|
+
* @param {Node} body - The body (BlockStatement or single statement).
|
|
6206
|
+
* @returns {string} WAT source of the body.
|
|
6207
|
+
*/
|
|
6208
|
+
bodySource(body) {
|
|
6209
|
+
if (body.type === 'BlockStatement') {
|
|
6210
|
+
return this.mapToSource(body.body).join('');
|
|
6211
|
+
}
|
|
6212
|
+
return this.toSource(body);
|
|
6213
|
+
}
|
|
6214
|
+
/**
|
|
6215
|
+
* Converts an IfStatement node to WAT.
|
|
6216
|
+
* @param {import("@babel/types").IfStatement} node - The Babel AST node.
|
|
6217
|
+
* @returns {string} WAT representation of the node.
|
|
6218
|
+
*/
|
|
6219
|
+
IfStatement(node) {
|
|
6220
|
+
const {
|
|
6221
|
+
test,
|
|
6222
|
+
consequent,
|
|
6223
|
+
alternate
|
|
6224
|
+
} = node;
|
|
6225
|
+
const {
|
|
6226
|
+
spaces
|
|
6227
|
+
} = this;
|
|
6228
|
+
let out = `${spaces}(if\n`;
|
|
6229
|
+
this.numSpaces++;
|
|
6230
|
+
out += this.getTestSource(test);
|
|
6231
|
+
out += `${this.spaces}(then\n`;
|
|
6232
|
+
this.numSpaces++;
|
|
6233
|
+
out += this.bodySource(consequent);
|
|
6234
|
+
this.numSpaces--;
|
|
6235
|
+
out += `${this.spaces})\n`;
|
|
6236
|
+
if (alternate) {
|
|
6237
|
+
out += `${this.spaces}(else\n`;
|
|
6238
|
+
this.numSpaces++;
|
|
6239
|
+
out += this.bodySource(alternate);
|
|
6240
|
+
this.numSpaces--;
|
|
6241
|
+
out += `${this.spaces})\n`;
|
|
6242
|
+
}
|
|
6243
|
+
this.numSpaces--;
|
|
6244
|
+
out += `${spaces})\n`;
|
|
6245
|
+
return out;
|
|
6246
|
+
}
|
|
6247
|
+
/**
|
|
6248
|
+
* Registers the current loop as innermost and returns its label names.
|
|
6249
|
+
* @param {'while'|'for'|'do'} kind - The loop kind.
|
|
6250
|
+
* @param {Node} [update] - The for-loop update expression, regenerated inline at each continue.
|
|
6251
|
+
* @returns {{exit: string, top: string, updateSrc: string}} The loop labels.
|
|
6252
|
+
*/
|
|
6253
|
+
pushLoop(kind, update) {
|
|
6254
|
+
const id = this.loopCounter++;
|
|
6255
|
+
const entry = {
|
|
6256
|
+
exit: `$exit${id}`,
|
|
6257
|
+
top: `$top${id}`,
|
|
6258
|
+
updateSrc: kind === 'for' && update ? this.toSource(update) : ''
|
|
6259
|
+
};
|
|
6260
|
+
this.loops.push(entry);
|
|
6261
|
+
return entry;
|
|
6262
|
+
}
|
|
6263
|
+
/**
|
|
6264
|
+
* Removes the innermost loop from the stack.
|
|
6265
|
+
* @returns {{exit: string, top: string, updateSrc: string}|undefined} The popped loop labels.
|
|
6266
|
+
*/
|
|
6267
|
+
popLoop() {
|
|
6268
|
+
return this.loops.pop();
|
|
6269
|
+
}
|
|
6270
|
+
/**
|
|
6271
|
+
* Converts a WhileStatement node to WAT.
|
|
6272
|
+
* @param {import("@babel/types").WhileStatement} node - The Babel AST node.
|
|
6273
|
+
* @returns {string} WAT representation of the node.
|
|
6274
|
+
*/
|
|
6275
|
+
WhileStatement(node) {
|
|
6276
|
+
const {
|
|
6277
|
+
test,
|
|
6278
|
+
body
|
|
6279
|
+
} = node;
|
|
6280
|
+
const {
|
|
6281
|
+
spaces
|
|
6282
|
+
} = this;
|
|
6283
|
+
const labels = this.pushLoop('while');
|
|
6284
|
+
let out = `${spaces}(block ${labels.exit}\n`;
|
|
6285
|
+
this.numSpaces++;
|
|
6286
|
+
out += `${this.spaces}(loop ${labels.top}\n`;
|
|
6287
|
+
this.numSpaces++;
|
|
6288
|
+
out += this.getTestSource(test);
|
|
6289
|
+
out += `${this.spaces}(i32.eqz)\n`;
|
|
6290
|
+
out += `${this.spaces}(br_if ${labels.exit})\n`;
|
|
6291
|
+
out += this.bodySource(body);
|
|
6292
|
+
out += `${this.spaces}(br ${labels.top})\n`;
|
|
6293
|
+
this.numSpaces--;
|
|
6294
|
+
out += `${this.spaces})\n`;
|
|
6295
|
+
this.numSpaces--;
|
|
6296
|
+
out += `${spaces})\n`;
|
|
6297
|
+
this.popLoop();
|
|
6298
|
+
return out;
|
|
6299
|
+
}
|
|
6300
|
+
/**
|
|
6301
|
+
* Converts a ForStatement node to WAT.
|
|
6302
|
+
* @param {import("@babel/types").ForStatement} node - The Babel AST node.
|
|
6303
|
+
* @returns {string} WAT representation of the node.
|
|
6304
|
+
*/
|
|
6305
|
+
ForStatement(node) {
|
|
6306
|
+
const {
|
|
6307
|
+
init,
|
|
6308
|
+
test,
|
|
6309
|
+
update,
|
|
6310
|
+
body
|
|
6311
|
+
} = node;
|
|
6312
|
+
const {
|
|
6313
|
+
spaces
|
|
6314
|
+
} = this;
|
|
6315
|
+
let out = init ? this.toSource(init) : '';
|
|
6316
|
+
const labels = this.pushLoop('for', update);
|
|
6317
|
+
out += `${spaces}(block ${labels.exit}\n`;
|
|
6318
|
+
this.numSpaces++;
|
|
6319
|
+
out += `${this.spaces}(loop ${labels.top}\n`;
|
|
6320
|
+
this.numSpaces++;
|
|
6321
|
+
if (test) {
|
|
6322
|
+
out += this.getTestSource(test);
|
|
6323
|
+
out += `${this.spaces}(i32.eqz)\n`;
|
|
6324
|
+
out += `${this.spaces}(br_if ${labels.exit})\n`;
|
|
6325
|
+
}
|
|
6326
|
+
out += this.bodySource(body);
|
|
6327
|
+
if (labels.updateSrc) {
|
|
6328
|
+
out += labels.updateSrc;
|
|
6329
|
+
}
|
|
6330
|
+
out += `${this.spaces}(br ${labels.top})\n`;
|
|
6331
|
+
this.numSpaces--;
|
|
6332
|
+
out += `${this.spaces})\n`;
|
|
6333
|
+
this.numSpaces--;
|
|
6334
|
+
out += `${spaces})\n`;
|
|
6335
|
+
this.popLoop();
|
|
6336
|
+
return out;
|
|
6337
|
+
}
|
|
6338
|
+
/**
|
|
6339
|
+
* Converts a DoWhileStatement node to WAT.
|
|
6340
|
+
* @param {import("@babel/types").DoWhileStatement} node - The Babel AST node.
|
|
6341
|
+
* @returns {string} WAT representation of the node.
|
|
6342
|
+
*/
|
|
6343
|
+
DoWhileStatement(node) {
|
|
6344
|
+
const {
|
|
6345
|
+
test,
|
|
6346
|
+
body
|
|
6347
|
+
} = node;
|
|
6348
|
+
const {
|
|
6349
|
+
spaces
|
|
6350
|
+
} = this;
|
|
6351
|
+
const labels = this.pushLoop('do');
|
|
6352
|
+
let out = `${spaces}(block ${labels.exit}\n`;
|
|
6353
|
+
this.numSpaces++;
|
|
6354
|
+
out += `${this.spaces}(loop ${labels.top}\n`;
|
|
6355
|
+
this.numSpaces++;
|
|
6356
|
+
out += this.bodySource(body);
|
|
6357
|
+
out += this.getTestSource(test);
|
|
6358
|
+
out += `${this.spaces}(i32.eqz)\n`;
|
|
6359
|
+
out += `${this.spaces}(br_if ${labels.exit})\n`;
|
|
6360
|
+
out += `${this.spaces}(br ${labels.top})\n`;
|
|
6361
|
+
this.numSpaces--;
|
|
6362
|
+
out += `${this.spaces})\n`;
|
|
6363
|
+
this.numSpaces--;
|
|
6364
|
+
out += `${spaces})\n`;
|
|
6365
|
+
this.popLoop();
|
|
6366
|
+
return out;
|
|
6367
|
+
}
|
|
6368
|
+
/**
|
|
6369
|
+
* Converts a BreakStatement node to WAT.
|
|
6370
|
+
* @param {import("@babel/types").BreakStatement} node - The Babel AST node.
|
|
6371
|
+
* @returns {string} WAT representation of the node.
|
|
6372
|
+
*/
|
|
6373
|
+
BreakStatement(node) {
|
|
6374
|
+
const {
|
|
6375
|
+
spaces
|
|
6376
|
+
} = this;
|
|
6377
|
+
if (node.label) {
|
|
6378
|
+
return `${spaces}(br $exit) ;; Unsupported labeled break "${node.label.name}"\n`;
|
|
6379
|
+
}
|
|
6380
|
+
const labels = this.loops[this.loops.length - 1];
|
|
6381
|
+
if (labels === undefined) {
|
|
6382
|
+
return `${spaces};; Unsupported break outside loop\n`;
|
|
6383
|
+
}
|
|
6384
|
+
return `${spaces}(br ${labels.exit})\n`;
|
|
6385
|
+
}
|
|
6386
|
+
/**
|
|
6387
|
+
* Converts a ContinueStatement node to WAT.
|
|
6388
|
+
* @param {import("@babel/types").ContinueStatement} node - The Babel AST node.
|
|
6389
|
+
* @returns {string} WAT representation of the node.
|
|
6390
|
+
*/
|
|
6391
|
+
ContinueStatement(node) {
|
|
6392
|
+
const {
|
|
6393
|
+
spaces
|
|
6394
|
+
} = this;
|
|
6395
|
+
if (node.label) {
|
|
6396
|
+
return `${spaces};; Unsupported labeled continue "${node.label.name}"\n`;
|
|
6397
|
+
}
|
|
6398
|
+
const labels = this.loops[this.loops.length - 1];
|
|
6399
|
+
if (labels === undefined) {
|
|
6400
|
+
return `${spaces};; Unsupported continue outside loop\n`;
|
|
6401
|
+
}
|
|
6402
|
+
let out = '';
|
|
6403
|
+
if (labels.updateSrc) {
|
|
6404
|
+
out += labels.updateSrc;
|
|
6405
|
+
}
|
|
6406
|
+
out += `${spaces}(br ${labels.top})\n`;
|
|
6407
|
+
return out;
|
|
6408
|
+
}
|
|
6409
|
+
/**
|
|
6410
|
+
* Converts a ReturnStatement node to WAT.
|
|
6411
|
+
* @param {import("@babel/types").ReturnStatement} node - The Babel AST node.
|
|
6412
|
+
* @returns {string} WAT representation of the node.
|
|
6413
|
+
*/
|
|
6414
|
+
ReturnStatement(node) {
|
|
6415
|
+
const {
|
|
6416
|
+
argument
|
|
6417
|
+
} = node;
|
|
6418
|
+
const {
|
|
6419
|
+
spaces
|
|
6420
|
+
} = this;
|
|
6421
|
+
let out = `${spaces}(return\n`;
|
|
6422
|
+
this.numSpaces++;
|
|
6423
|
+
let expr = '';
|
|
6424
|
+
if (!argument) {
|
|
6425
|
+
expr = `${this.spaces}(${CONST_TYPE[this.currentType]} 0)\n`;
|
|
6426
|
+
} else {
|
|
6427
|
+
expr = this.toSource(argument);
|
|
6428
|
+
}
|
|
6429
|
+
out += expr;
|
|
6430
|
+
this.numSpaces--;
|
|
6431
|
+
out += `${spaces})\n`;
|
|
6432
|
+
return out;
|
|
6433
|
+
}
|
|
6434
|
+
/**
|
|
6435
|
+
* Converts a BinaryExpression node to WAT.
|
|
6436
|
+
* @param {import("@babel/types").BinaryExpression} node - The Babel AST node.
|
|
6437
|
+
* @returns {string} WAT representation of the node.
|
|
6438
|
+
*/
|
|
6439
|
+
BinaryExpression(node) {
|
|
6440
|
+
const {
|
|
6441
|
+
left,
|
|
6442
|
+
operator,
|
|
6443
|
+
right
|
|
6444
|
+
} = node;
|
|
6445
|
+
const {
|
|
6446
|
+
spaces
|
|
6447
|
+
} = this;
|
|
6448
|
+
const t = this.currentType;
|
|
6449
|
+
if (operator === '%') {
|
|
6450
|
+
return this.percentToSource(node);
|
|
6451
|
+
}
|
|
6452
|
+
let opCode = '';
|
|
6453
|
+
if (CMP_OPERATORS.includes(operator)) {
|
|
6454
|
+
const opName = {
|
|
6455
|
+
'<': 'lt',
|
|
6456
|
+
'<=': 'le',
|
|
6457
|
+
'>': 'gt',
|
|
6458
|
+
'>=': 'ge',
|
|
6459
|
+
'==': 'eq',
|
|
6460
|
+
'!=': 'ne'
|
|
6461
|
+
}[operator];
|
|
6462
|
+
opCode = CMP_OP[t][opName];
|
|
6463
|
+
} else {
|
|
6464
|
+
const opName = {
|
|
6465
|
+
'+': 'add',
|
|
6466
|
+
'-': 'sub',
|
|
6467
|
+
'*': 'mul',
|
|
6468
|
+
'/': 'div'
|
|
6469
|
+
}[operator];
|
|
6470
|
+
if (!opName) {
|
|
6471
|
+
return `${spaces}(${CONST_TYPE[t]} 0.0) ;; Unsupported operator: ${operator}\n`;
|
|
6472
|
+
}
|
|
6473
|
+
opCode = ARITH_OP[t][opName];
|
|
6474
|
+
}
|
|
6475
|
+
let out = `${spaces}(${opCode}\n`;
|
|
6476
|
+
this.numSpaces++;
|
|
6477
|
+
out += this.toSource(left) + this.toSource(right);
|
|
6478
|
+
this.numSpaces--;
|
|
6479
|
+
out += `${spaces})\n`;
|
|
6480
|
+
return out;
|
|
6481
|
+
}
|
|
6482
|
+
/**
|
|
6483
|
+
* Converts a `%` (modulo) expression, emulating float modulo via truncation.
|
|
6484
|
+
* @param {import("@babel/types").BinaryExpression} node - The Babel AST node.
|
|
6485
|
+
* @returns {string} WAT representation of the node.
|
|
6486
|
+
*/
|
|
6487
|
+
percentToSource(node) {
|
|
6488
|
+
const {
|
|
6489
|
+
left,
|
|
6490
|
+
right
|
|
6491
|
+
} = node;
|
|
6492
|
+
const {
|
|
6493
|
+
spaces
|
|
6494
|
+
} = this;
|
|
6495
|
+
const t = this.currentType;
|
|
6496
|
+
if (t === 'i32' || t === 'i64') {
|
|
6497
|
+
let _out = `${spaces}(${t}.rem_s\n`;
|
|
6498
|
+
this.numSpaces++;
|
|
6499
|
+
_out += this.toSource(left) + this.toSource(right);
|
|
6500
|
+
this.numSpaces--;
|
|
6501
|
+
_out += `${spaces})\n`;
|
|
6502
|
+
return _out;
|
|
6503
|
+
}
|
|
6504
|
+
const a = ARITH_OP[t];
|
|
6505
|
+
const leftDiv = this.atDepth(5, () => this.toSource(left));
|
|
6506
|
+
const rightDiv = this.atDepth(5, () => this.toSource(right));
|
|
6507
|
+
const leftTop = this.atDepth(1, () => this.toSource(left));
|
|
6508
|
+
const rightMul = this.atDepth(2, () => this.toSource(right));
|
|
6509
|
+
const indent = n => ' '.repeat(this.numSpaces + n);
|
|
6510
|
+
let out = `${spaces}(${a.add}\n`;
|
|
6511
|
+
out += leftTop;
|
|
6512
|
+
out += `${indent(1)}(${NEG_OP[t]}\n`;
|
|
6513
|
+
out += `${indent(2)}(${a.mul}\n`;
|
|
6514
|
+
out += `${indent(3)}(${MATH_OP[t].trunc}\n`;
|
|
6515
|
+
out += `${indent(4)}(${a.div}\n`;
|
|
6516
|
+
out += leftDiv + rightDiv;
|
|
6517
|
+
out += `${indent(4)})\n`;
|
|
6518
|
+
out += `${indent(3)})\n`;
|
|
6519
|
+
out += rightMul;
|
|
6520
|
+
out += `${indent(2)})\n`;
|
|
6521
|
+
out += `${indent(1)})\n`;
|
|
6522
|
+
out += `${spaces})\n`;
|
|
6523
|
+
return out;
|
|
6524
|
+
}
|
|
6525
|
+
/**
|
|
6526
|
+
* Converts an UnaryExpression node to WAT.
|
|
6527
|
+
* @param {import("@babel/types").UnaryExpression} node - The Babel AST node.
|
|
6528
|
+
* @returns {string} WAT representation of the node.
|
|
6529
|
+
*/
|
|
6530
|
+
UnaryExpression(node) {
|
|
6531
|
+
const {
|
|
6532
|
+
argument,
|
|
6533
|
+
operator
|
|
6534
|
+
} = node;
|
|
6535
|
+
const {
|
|
6536
|
+
spaces
|
|
6537
|
+
} = this;
|
|
6538
|
+
const t = this.currentType;
|
|
6539
|
+
if (operator === '-') {
|
|
6540
|
+
if (NEG_OP[t]) {
|
|
6541
|
+
let _out2 = `${spaces}(${NEG_OP[t]}\n`;
|
|
6542
|
+
this.numSpaces++;
|
|
6543
|
+
_out2 += this.toSource(argument);
|
|
6544
|
+
this.numSpaces--;
|
|
6545
|
+
_out2 += `${spaces})\n`;
|
|
6546
|
+
return _out2;
|
|
6547
|
+
}
|
|
6548
|
+
let out = `${spaces}(${ARITH_OP[t].sub}\n`;
|
|
6549
|
+
this.numSpaces++;
|
|
6550
|
+
out += `${this.spaces}(${CONST_TYPE[t]} 0)\n`;
|
|
6551
|
+
out += this.toSource(argument);
|
|
6552
|
+
this.numSpaces--;
|
|
6553
|
+
out += `${spaces})\n`;
|
|
6554
|
+
return out;
|
|
6555
|
+
}
|
|
6556
|
+
if (operator === '!') {
|
|
6557
|
+
const op = this.isBooleanI32(argument) ? 'i32.eqz' : EQZ_OP[t];
|
|
6558
|
+
let out = `${spaces}(${op}\n`;
|
|
6559
|
+
this.numSpaces++;
|
|
6560
|
+
out += this.toSource(argument);
|
|
6561
|
+
this.numSpaces--;
|
|
6562
|
+
out += `${spaces})\n`;
|
|
6563
|
+
return out;
|
|
6564
|
+
}
|
|
6565
|
+
return `${spaces}(${CONST_TYPE[t]} 0.0) ;; Unsupported unary operator: ${operator}\n`;
|
|
6566
|
+
}
|
|
6567
|
+
/**
|
|
6568
|
+
* Converts a VariableDeclaration node to WAT.
|
|
6569
|
+
* @param {import("@babel/types").VariableDeclaration} node - The Babel AST node.
|
|
6570
|
+
* @returns {string} WAT representation of the node.
|
|
6571
|
+
*/
|
|
6572
|
+
VariableDeclaration(node) {
|
|
6573
|
+
const {
|
|
6574
|
+
declarations
|
|
6575
|
+
} = node;
|
|
6576
|
+
return declarations.map(decl => this.toSource(decl)).join('');
|
|
6577
|
+
}
|
|
6578
|
+
/**
|
|
6579
|
+
* Converts a VariableDeclarator node to WAT.
|
|
6580
|
+
* @param {import("@babel/types").VariableDeclarator} node - The Babel AST node.
|
|
6581
|
+
* @returns {string} WAT representation of the node.
|
|
6582
|
+
*/
|
|
6583
|
+
VariableDeclarator(node) {
|
|
6584
|
+
const {
|
|
6585
|
+
id,
|
|
6586
|
+
init
|
|
6587
|
+
} = node;
|
|
6588
|
+
const {
|
|
6589
|
+
spaces
|
|
6590
|
+
} = this;
|
|
6591
|
+
if (id.type === 'Identifier' && init && init.type === 'NewExpression' && init.callee.type === 'Identifier' && this.classes[init.callee.name]) {
|
|
6592
|
+
this.instances[id.name] = init.callee.name;
|
|
6593
|
+
}
|
|
6594
|
+
let out = `${spaces}(local.set $${id.name}\n`;
|
|
6595
|
+
this.numSpaces++;
|
|
6596
|
+
if (init) {
|
|
6597
|
+
out += this.toSource(init);
|
|
6598
|
+
} else {
|
|
6599
|
+
out += `${this.spaces}(${CONST_TYPE[this.currentType]} 0) ;; Initialized as zero.\n`;
|
|
6600
|
+
}
|
|
6601
|
+
this.numSpaces--;
|
|
6602
|
+
out += `${spaces})\n`;
|
|
6603
|
+
return out;
|
|
6604
|
+
}
|
|
6605
|
+
/**
|
|
6606
|
+
* Converts an UpdateExpression node to WAT.
|
|
6607
|
+
* @param {import("@babel/types").UpdateExpression} node - The Babel AST node.
|
|
6608
|
+
* @returns {string} WAT representation of the node.
|
|
6609
|
+
*/
|
|
6610
|
+
UpdateExpression(node) {
|
|
6611
|
+
const {
|
|
6612
|
+
argument,
|
|
6613
|
+
operator
|
|
6614
|
+
} = node;
|
|
6615
|
+
const {
|
|
6616
|
+
spaces
|
|
6617
|
+
} = this;
|
|
6618
|
+
const t = this.currentType;
|
|
6619
|
+
if (argument.type !== 'Identifier') {
|
|
6620
|
+
return `${spaces}(${CONST_TYPE[t]} 0.0) ;; Unsupported UpdateExpression target ${argument.type}\n`;
|
|
6621
|
+
}
|
|
6622
|
+
const op = operator === '++' ? ARITH_OP[t].add : ARITH_OP[t].sub;
|
|
6623
|
+
let out = `${spaces}(local.set $${argument.name}\n`;
|
|
6624
|
+
this.numSpaces++;
|
|
6625
|
+
out += `${this.spaces}(${op}\n`;
|
|
6626
|
+
this.numSpaces++;
|
|
6627
|
+
out += `${this.spaces}(local.get $${argument.name})\n`;
|
|
6628
|
+
out += `${this.spaces}(${CONST_TYPE[t]} 1)\n`;
|
|
6629
|
+
this.numSpaces--;
|
|
6630
|
+
out += `${this.spaces})\n`;
|
|
6631
|
+
this.numSpaces--;
|
|
6632
|
+
out += `${spaces})\n`;
|
|
6633
|
+
return out;
|
|
6634
|
+
}
|
|
6635
|
+
/**
|
|
6636
|
+
* Converts an AssignmentExpression node to WAT.
|
|
6637
|
+
* @param {import("@babel/types").AssignmentExpression} node - The Babel AST node.
|
|
6638
|
+
* @returns {string} WAT representation of the node.
|
|
6639
|
+
*/
|
|
6640
|
+
AssignmentExpression(node) {
|
|
6641
|
+
const {
|
|
6642
|
+
left,
|
|
6643
|
+
operator,
|
|
6644
|
+
right
|
|
6645
|
+
} = node;
|
|
6646
|
+
const {
|
|
6647
|
+
spaces
|
|
6648
|
+
} = this;
|
|
6649
|
+
if (operator !== '=') {
|
|
6650
|
+
return `${spaces}(${CONST_TYPE[this.currentType]} 0.0) ;; Unsupported assignment operator: ${operator}\n`;
|
|
6651
|
+
}
|
|
6652
|
+
if (left.type === 'Identifier') {
|
|
6653
|
+
let out = `${spaces}(local.set $${left.name}\n`;
|
|
6654
|
+
this.numSpaces++;
|
|
6655
|
+
out += this.toSource(right);
|
|
6656
|
+
this.numSpaces--;
|
|
6657
|
+
out += `${spaces})\n`;
|
|
6658
|
+
return out;
|
|
6659
|
+
}
|
|
6660
|
+
if (left.type === 'MemberExpression') {
|
|
6661
|
+
const member = left;
|
|
6662
|
+
const pointerName = this.instancePointerName(member.object);
|
|
6663
|
+
const fieldName = member.property.type === 'Identifier' ? member.property.name : member.property.value;
|
|
6664
|
+
if (pointerName) {
|
|
6665
|
+
const clsName = this.resolveInstanceClass(member.object, fieldName);
|
|
6666
|
+
if (clsName && fieldName in this.classes[clsName].offsets) {
|
|
6667
|
+
return this.classFieldStoreSource(pointerName, right, fieldName, this.classes[clsName]);
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
const arr = member.object.type === 'Identifier' ? this.arrays[member.object.name] : null;
|
|
6671
|
+
if (arr) {
|
|
6672
|
+
const indexSrc = this.arrayIndexSource(arr, member);
|
|
6673
|
+
let out = `${spaces}(f32.store\n`;
|
|
6674
|
+
this.numSpaces++;
|
|
6675
|
+
out += this.arrayAddressSource(arr, indexSrc);
|
|
6676
|
+
out += this.toSource(right);
|
|
6677
|
+
this.numSpaces--;
|
|
6678
|
+
out += `${spaces})\n`;
|
|
6679
|
+
return out;
|
|
6680
|
+
}
|
|
6681
|
+
const objectName = member.object.type === 'Identifier' ? member.object.name : member.object.type;
|
|
6682
|
+
return `${spaces}(${CONST_TYPE[this.currentType]} 0.0) ;; Unsupported store target ${objectName}\n`;
|
|
6683
|
+
}
|
|
6684
|
+
return `${spaces}(${CONST_TYPE[this.currentType]} 0.0) ;; Unsupported assignment target ${left.type}\n`;
|
|
6685
|
+
}
|
|
6686
|
+
/**
|
|
6687
|
+
* Produces an i32 source for the element index of an array/object member access.
|
|
6688
|
+
* @param {Object} arr - The registered array/object data.
|
|
6689
|
+
* @param {import("@babel/types").MemberExpression} member - The member expression.
|
|
6690
|
+
* @returns {string} WAT source that pushes the element index.
|
|
6691
|
+
*/
|
|
6692
|
+
arrayIndexSource(arr, member) {
|
|
6693
|
+
if (!arr.isObject) {
|
|
6694
|
+
if (!member.computed) {
|
|
6695
|
+
return `${this.spaces};; Unsupported ${member.object.name}.${member.property.name} on array\n`;
|
|
6696
|
+
}
|
|
6697
|
+
return this.toI32(member.property);
|
|
6698
|
+
}
|
|
6699
|
+
const key = member.computed && member.property.type === 'StringLiteral' ? member.property.value : member.property.name;
|
|
6700
|
+
const index = arr.keys.get(key);
|
|
6701
|
+
if (index === undefined) {
|
|
6702
|
+
return `${this.spaces}; Unknown key "${member.property.name}"\n`;
|
|
6703
|
+
}
|
|
6704
|
+
return `${this.spaces}(i32.const ${index})\n`;
|
|
6705
|
+
}
|
|
6706
|
+
/**
|
|
6707
|
+
* Truncates a numeric index expression to i32, passing i32 values through.
|
|
6708
|
+
* @param {Node} expr - The index expression.
|
|
6709
|
+
* @returns {string} WAT source that pushes an i32 index.
|
|
6710
|
+
*/
|
|
6711
|
+
toI32(expr) {
|
|
6712
|
+
const {
|
|
6713
|
+
spaces
|
|
6714
|
+
} = this;
|
|
6715
|
+
const t = this.currentType;
|
|
6716
|
+
if (t === 'i32') {
|
|
6717
|
+
return this.toSource(expr);
|
|
6718
|
+
}
|
|
6719
|
+
if (t !== 'f32' && t !== 'f64') {
|
|
6720
|
+
return `${spaces};; Unsupported index type ${t}\n`;
|
|
6721
|
+
}
|
|
6722
|
+
const truncExpr = t === 'f32' ? 'i32.trunc_f32_s' : 'i32.trunc_f64_s';
|
|
6723
|
+
let out = `${spaces}(${truncExpr}\n`;
|
|
6724
|
+
this.numSpaces++;
|
|
6725
|
+
out += this.toSource(expr);
|
|
6726
|
+
this.numSpaces--;
|
|
6727
|
+
out += `${spaces})\n`;
|
|
6728
|
+
return out;
|
|
6729
|
+
}
|
|
6730
|
+
/**
|
|
6731
|
+
* Computes the byte address of `baseOffset + elementIndex * 4`.
|
|
6732
|
+
* @param {Object} arr - The registered array/object data.
|
|
6733
|
+
* @param {string} indexSrc - The i32 element index source.
|
|
6734
|
+
* @returns {string} WAT source that pushes the byte address.
|
|
6735
|
+
*/
|
|
6736
|
+
arrayAddressSource(arr, indexSrc) {
|
|
6737
|
+
const {
|
|
6738
|
+
spaces
|
|
6739
|
+
} = this;
|
|
6740
|
+
let out = `${spaces}(i32.add\n`;
|
|
6741
|
+
this.numSpaces++;
|
|
6742
|
+
out += `${this.spaces}(i32.const ${arr.offset})\n`;
|
|
6743
|
+
out += `${this.spaces}(i32.mul\n`;
|
|
6744
|
+
this.numSpaces++;
|
|
6745
|
+
out += indexSrc;
|
|
6746
|
+
out += `${this.spaces}(i32.const 4)\n`;
|
|
6747
|
+
this.numSpaces--;
|
|
6748
|
+
out += `${this.spaces})\n`;
|
|
6749
|
+
this.numSpaces--;
|
|
6750
|
+
out += `${spaces})\n`;
|
|
6751
|
+
return out;
|
|
6752
|
+
}
|
|
6753
|
+
/**
|
|
6754
|
+
* Returns the WAT local name holding an instance pointer.
|
|
6755
|
+
* @param {Node} objectNode - The object expression.
|
|
6756
|
+
* @returns {string|null} The local name.
|
|
6757
|
+
*/
|
|
6758
|
+
instancePointerName(objectNode) {
|
|
6759
|
+
const isThis = objectNode.type === 'ThisExpression' || objectNode.type === 'Identifier' && objectNode.name === 'this';
|
|
6760
|
+
return isThis ? 'this' : objectNode.type === 'Identifier' ? objectNode.name : null;
|
|
6761
|
+
}
|
|
6762
|
+
/**
|
|
6763
|
+
* Finds the class of an instance via `this`, a tracked `new` variable, or a unique field-name match.
|
|
6764
|
+
* @param {Node} objectNode - The member/call object expression.
|
|
6765
|
+
* @param {string} propertyName - The accessed field or method name.
|
|
6766
|
+
* @returns {string|null} The class name.
|
|
6767
|
+
*/
|
|
6768
|
+
resolveInstanceClass(objectNode, propertyName) {
|
|
6769
|
+
const pointerName = this.instancePointerName(objectNode);
|
|
6770
|
+
if (pointerName === 'this') {
|
|
6771
|
+
return this.currentClass;
|
|
6772
|
+
}
|
|
6773
|
+
if (pointerName === null) {
|
|
6774
|
+
return null;
|
|
6775
|
+
}
|
|
6776
|
+
if (this.instances[pointerName]) {
|
|
6777
|
+
return this.instances[pointerName];
|
|
6778
|
+
}
|
|
6779
|
+
if (pointerName in this.arrays) {
|
|
6780
|
+
return null;
|
|
6781
|
+
}
|
|
6782
|
+
const clsNames = Object.keys(this.classes).filter(n => propertyName in this.classes[n].offsets);
|
|
6783
|
+
return clsNames.length === 1 ? clsNames[0] : null;
|
|
6784
|
+
}
|
|
6785
|
+
/**
|
|
6786
|
+
* Finds the class of the object a method call is dispatched on.
|
|
6787
|
+
* @param {Node} objectNode - The call object expression.
|
|
6788
|
+
* @param {string} methodName - The called method name.
|
|
6789
|
+
* @returns {string|null} The class name.
|
|
6790
|
+
*/
|
|
6791
|
+
resolveInstanceMethodClass(objectNode, methodName) {
|
|
6792
|
+
const pointerName = this.instancePointerName(objectNode);
|
|
6793
|
+
if (pointerName === 'this') {
|
|
6794
|
+
return this.currentClass && this.classes[this.currentClass].methods.has(methodName) ? this.currentClass : null;
|
|
6795
|
+
}
|
|
6796
|
+
if (pointerName === null) {
|
|
6797
|
+
return null;
|
|
6798
|
+
}
|
|
6799
|
+
if (this.instances[pointerName]) {
|
|
6800
|
+
return this.instances[pointerName];
|
|
6801
|
+
}
|
|
6802
|
+
if (pointerName in this.arrays) {
|
|
6803
|
+
return null;
|
|
6804
|
+
}
|
|
6805
|
+
const clsNames = Object.keys(this.classes).filter(n => this.classes[n].methods.has(methodName));
|
|
6806
|
+
return clsNames.length === 1 ? clsNames[0] : null;
|
|
6807
|
+
}
|
|
6808
|
+
/**
|
|
6809
|
+
* Produces the WAT source for the heap byte address of a class field.
|
|
6810
|
+
* @param {string} pointerName - The local holding the instance pointer.
|
|
6811
|
+
* @param {string} fieldName - The field name.
|
|
6812
|
+
* @param {Object} cls - The class layout.
|
|
6813
|
+
* @returns {string} WAT source that pushes the byte address.
|
|
6814
|
+
*/
|
|
6815
|
+
classFieldAddressSource(pointerName, fieldName, cls) {
|
|
6816
|
+
const {
|
|
6817
|
+
spaces
|
|
6818
|
+
} = this;
|
|
6819
|
+
let out = `${spaces}(i32.add\n`;
|
|
6820
|
+
this.numSpaces++;
|
|
6821
|
+
out += `${this.spaces}(i32.trunc_f32_s\n`;
|
|
6822
|
+
this.numSpaces++;
|
|
6823
|
+
out += `${this.spaces}(local.get $${pointerName})\n`;
|
|
6824
|
+
this.numSpaces--;
|
|
6825
|
+
out += `${this.spaces})\n`;
|
|
6826
|
+
out += `${this.spaces}(i32.const ${cls.offsets[fieldName]})\n`;
|
|
6827
|
+
this.numSpaces--;
|
|
6828
|
+
out += `${spaces})\n`;
|
|
6829
|
+
return out;
|
|
6830
|
+
}
|
|
6831
|
+
/**
|
|
6832
|
+
* Produces the WAT source for loading a class field from the heap.
|
|
6833
|
+
* @param {string} pointerName - The local holding the instance pointer.
|
|
6834
|
+
* @param {string} fieldName - The field name.
|
|
6835
|
+
* @param {Object} cls - The class layout.
|
|
6836
|
+
* @returns {string} WAT source that pushes the field value.
|
|
6837
|
+
*/
|
|
6838
|
+
classFieldLoadSource(pointerName, fieldName, cls) {
|
|
6839
|
+
const {
|
|
6840
|
+
spaces
|
|
6841
|
+
} = this;
|
|
6842
|
+
let out = `${spaces}(f32.load\n`;
|
|
6843
|
+
this.numSpaces++;
|
|
6844
|
+
out += this.classFieldAddressSource(pointerName, fieldName, cls);
|
|
6845
|
+
this.numSpaces--;
|
|
6846
|
+
out += `${spaces})\n`;
|
|
6847
|
+
return out;
|
|
6848
|
+
}
|
|
6849
|
+
/**
|
|
6850
|
+
* Produces the WAT source for storing a value into a class field.
|
|
6851
|
+
* @param {string} pointerName - The local holding the instance pointer.
|
|
6852
|
+
* @param {Node} valueNode - The value expression.
|
|
6853
|
+
* @param {string} fieldName - The field name.
|
|
6854
|
+
* @param {Object} cls - The class layout.
|
|
6855
|
+
* @returns {string} WAT source that performs the store.
|
|
6856
|
+
*/
|
|
6857
|
+
classFieldStoreSource(pointerName, valueNode, fieldName, cls) {
|
|
6858
|
+
const {
|
|
6859
|
+
spaces
|
|
6860
|
+
} = this;
|
|
6861
|
+
let out = `${spaces}(f32.store\n`;
|
|
6862
|
+
this.numSpaces++;
|
|
6863
|
+
out += this.classFieldAddressSource(pointerName, fieldName, cls);
|
|
6864
|
+
out += this.toSource(valueNode);
|
|
6865
|
+
this.numSpaces--;
|
|
6866
|
+
out += `${spaces})\n`;
|
|
6867
|
+
return out;
|
|
6868
|
+
}
|
|
6869
|
+
/**
|
|
6870
|
+
* Converts a MemberExpression node (array/object element read) to WAT.
|
|
6871
|
+
* @param {import("@babel/types").MemberExpression} node - The Babel AST node.
|
|
6872
|
+
* @returns {string} WAT representation of the node.
|
|
6873
|
+
*/
|
|
6874
|
+
MemberExpression(node) {
|
|
6875
|
+
const {
|
|
6876
|
+
object,
|
|
6877
|
+
property
|
|
6878
|
+
} = node;
|
|
6879
|
+
const {
|
|
6880
|
+
spaces
|
|
6881
|
+
} = this;
|
|
6882
|
+
const pointerName = this.instancePointerName(object);
|
|
6883
|
+
const fieldName = property.type === 'Identifier' ? property.name : property.value;
|
|
6884
|
+
if (pointerName === 'this') {
|
|
6885
|
+
const _clsName = this.resolveInstanceClass(object, fieldName);
|
|
6886
|
+
if (_clsName && fieldName in this.classes[_clsName].offsets) {
|
|
6887
|
+
return this.classFieldLoadSource('this', fieldName, this.classes[_clsName]);
|
|
6888
|
+
}
|
|
6889
|
+
}
|
|
6890
|
+
if (object.type !== 'Identifier') {
|
|
6891
|
+
return `${spaces}(f32.const 0.0) ;; Unsupported member access on ${object.type}\n`;
|
|
6892
|
+
}
|
|
6893
|
+
const arr = this.arrays[object.name];
|
|
6894
|
+
if (arr) {
|
|
6895
|
+
const indexSrc = this.arrayIndexSource(arr, node);
|
|
6896
|
+
let out = `${spaces}(f32.load\n`;
|
|
6897
|
+
this.numSpaces++;
|
|
6898
|
+
out += this.arrayAddressSource(arr, indexSrc);
|
|
6899
|
+
this.numSpaces--;
|
|
6900
|
+
out += `${spaces})\n`;
|
|
6901
|
+
return out;
|
|
6902
|
+
}
|
|
6903
|
+
const clsName = this.resolveInstanceClass(object, fieldName);
|
|
6904
|
+
if (clsName && fieldName in this.classes[clsName].offsets) {
|
|
6905
|
+
return this.classFieldLoadSource(object.name, fieldName, this.classes[clsName]);
|
|
6906
|
+
}
|
|
6907
|
+
return `${spaces}(f32.const 0.0) ;; Unsupported member access on unknown "${object.name}"\n`;
|
|
6908
|
+
}
|
|
6909
|
+
/**
|
|
6910
|
+
* Converts a ConditionalExpression (ternary) node to WAT.
|
|
6911
|
+
* @param {import("@babel/types").ConditionalExpression} node - The Babel AST node.
|
|
6912
|
+
* @returns {string} WAT representation of the node.
|
|
6913
|
+
*/
|
|
6914
|
+
ConditionalExpression(node) {
|
|
6915
|
+
const {
|
|
6916
|
+
test,
|
|
6917
|
+
consequent,
|
|
6918
|
+
alternate
|
|
6919
|
+
} = node;
|
|
6920
|
+
const {
|
|
6921
|
+
spaces
|
|
6922
|
+
} = this;
|
|
6923
|
+
const t = this.currentType;
|
|
6924
|
+
let out = `${spaces}(if (result ${t})\n`;
|
|
6925
|
+
this.numSpaces++;
|
|
6926
|
+
out += this.getTestSource(test);
|
|
6927
|
+
out += `${this.spaces}(then\n`;
|
|
6928
|
+
this.numSpaces++;
|
|
6929
|
+
out += this.toSource(consequent);
|
|
6930
|
+
this.numSpaces--;
|
|
6931
|
+
out += `${this.spaces})\n`;
|
|
6932
|
+
out += `${this.spaces}(else\n`;
|
|
6933
|
+
this.numSpaces++;
|
|
6934
|
+
out += this.toSource(alternate);
|
|
6935
|
+
this.numSpaces--;
|
|
6936
|
+
out += `${this.spaces})\n`;
|
|
6937
|
+
this.numSpaces--;
|
|
6938
|
+
out += `${spaces})\n`;
|
|
6939
|
+
return out;
|
|
6940
|
+
}
|
|
6941
|
+
/**
|
|
6942
|
+
* Lowers `&&`/`||` to an if/then/else that short-circuits on the left operand.
|
|
6943
|
+
* @param {import("@babel/types").LogicalExpression} node - The Babel AST node.
|
|
6944
|
+
* @returns {string} WAT representation of the node.
|
|
6945
|
+
*/
|
|
6946
|
+
LogicalExpression(node) {
|
|
6947
|
+
const {
|
|
6948
|
+
left,
|
|
6949
|
+
operator,
|
|
6950
|
+
right
|
|
6951
|
+
} = node;
|
|
6952
|
+
const {
|
|
6953
|
+
spaces
|
|
6954
|
+
} = this;
|
|
6955
|
+
const resultType = this.isBooleanI32(left) && this.isBooleanI32(right) ? 'i32' : this.currentType;
|
|
6956
|
+
const thenOperand = operator === '&&' ? right : left;
|
|
6957
|
+
const elseOperand = operator === '&&' ? left : right;
|
|
6958
|
+
let out = `${spaces}(if (result ${resultType})\n`;
|
|
6959
|
+
this.numSpaces++;
|
|
6960
|
+
out += this.getTestSource(left);
|
|
6961
|
+
out += `${this.spaces}(then\n`;
|
|
6962
|
+
this.numSpaces++;
|
|
6963
|
+
out += this.toSource(thenOperand);
|
|
6964
|
+
this.numSpaces--;
|
|
6965
|
+
out += `${this.spaces})\n`;
|
|
6966
|
+
out += `${this.spaces}(else\n`;
|
|
6967
|
+
this.numSpaces++;
|
|
6968
|
+
out += this.toSource(elseOperand);
|
|
6969
|
+
this.numSpaces--;
|
|
6970
|
+
out += `${this.spaces})\n`;
|
|
6971
|
+
this.numSpaces--;
|
|
6972
|
+
out += `${spaces})\n`;
|
|
6973
|
+
return out;
|
|
6974
|
+
}
|
|
6975
|
+
/**
|
|
6976
|
+
* Converts an ExpressionStatement node to WAT, dropping unused values.
|
|
6977
|
+
* @param {import("@babel/types").ExpressionStatement} node - The Babel AST node.
|
|
6978
|
+
* @returns {string} WAT representation of the node.
|
|
6979
|
+
*/
|
|
6980
|
+
ExpressionStatement(node) {
|
|
6981
|
+
const {
|
|
6982
|
+
expression
|
|
6983
|
+
} = node;
|
|
6984
|
+
if (expression.type === 'AssignmentExpression' || expression.type === 'UpdateExpression') {
|
|
6985
|
+
return this.toSource(expression);
|
|
6986
|
+
}
|
|
6987
|
+
const {
|
|
6988
|
+
spaces
|
|
6989
|
+
} = this;
|
|
6990
|
+
let out = `${spaces}(drop\n`;
|
|
6991
|
+
this.numSpaces++;
|
|
6992
|
+
out += this.toSource(expression);
|
|
6993
|
+
this.numSpaces--;
|
|
6994
|
+
out += `${spaces})\n`;
|
|
6995
|
+
return out;
|
|
6996
|
+
}
|
|
6997
|
+
/**
|
|
6998
|
+
* Converts an Identifier node to WAT.
|
|
6999
|
+
* @param {import("@babel/types").Identifier} node - The Babel AST node.
|
|
7000
|
+
* @returns {string} WAT representation of the node.
|
|
7001
|
+
*/
|
|
7002
|
+
Identifier(node) {
|
|
7003
|
+
const {
|
|
7004
|
+
spaces
|
|
7005
|
+
} = this;
|
|
7006
|
+
const t = this.currentType;
|
|
7007
|
+
if (node.name === 'this') {
|
|
7008
|
+
return `${spaces}(local.get $this)\n`;
|
|
7009
|
+
}
|
|
7010
|
+
if (t === 'f32' || t === 'f64') {
|
|
7011
|
+
if (node.name === 'Infinity') {
|
|
7012
|
+
return `${spaces}(${CONST_TYPE[t]} inf)\n`;
|
|
7013
|
+
}
|
|
7014
|
+
if (node.name === 'NaN') {
|
|
7015
|
+
return `${spaces}(${CONST_TYPE[t]} nan)\n`;
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
return `${spaces}(local.get $${node.name})\n`;
|
|
7019
|
+
}
|
|
7020
|
+
/**
|
|
7021
|
+
* Converts a NumericLiteral node to WAT.
|
|
7022
|
+
* @param {import("@babel/types").NumericLiteral} node - The Babel AST node.
|
|
7023
|
+
* @returns {string} WAT representation of the node.
|
|
7024
|
+
*/
|
|
7025
|
+
NumericLiteral(node) {
|
|
7026
|
+
const {
|
|
7027
|
+
spaces
|
|
7028
|
+
} = this;
|
|
7029
|
+
const t = this.currentType;
|
|
7030
|
+
const constExpr = CONST_TYPE[t];
|
|
7031
|
+
const value = parseFloat(node.value);
|
|
7032
|
+
if (isNaN(value)) {
|
|
7033
|
+
return `${spaces}(${constExpr} 0.0) ;; Invalid literal value: ${node.value}\n`;
|
|
7034
|
+
}
|
|
7035
|
+
if (t === 'i32' || t === 'i64') {
|
|
7036
|
+
return `${spaces}(${constExpr} ${Math.trunc(value)})\n`;
|
|
7037
|
+
}
|
|
7038
|
+
return `${spaces}(${constExpr} ${value})\n`;
|
|
7039
|
+
}
|
|
7040
|
+
/**
|
|
7041
|
+
* Converts a CallExpression node to WAT.
|
|
7042
|
+
* @param {import("@babel/types").CallExpression} node - The Babel AST node.
|
|
7043
|
+
* @returns {string} WAT representation of the node.
|
|
7044
|
+
*/
|
|
7045
|
+
CallExpression(node) {
|
|
7046
|
+
const {
|
|
7047
|
+
callee,
|
|
7048
|
+
arguments: args
|
|
7049
|
+
} = node;
|
|
7050
|
+
const {
|
|
7051
|
+
spaces
|
|
7052
|
+
} = this;
|
|
7053
|
+
if (callee.type === 'MemberExpression') {
|
|
7054
|
+
const methodName = callee.property.type === 'Identifier' ? callee.property.name : callee.property.value;
|
|
7055
|
+
if (callee.object.type === 'Identifier' && callee.object.name === 'Math') {
|
|
7056
|
+
var _MATH_OP$this$current;
|
|
7057
|
+
const mathOp = (_MATH_OP$this$current = MATH_OP[this.currentType]) == null ? void 0 : _MATH_OP$this$current[methodName];
|
|
7058
|
+
if (!mathOp) {
|
|
7059
|
+
return `${spaces}(f32.const 0.0) ;; Unsupported Math.${methodName} for ${this.currentType}\n`;
|
|
7060
|
+
}
|
|
7061
|
+
let _out3 = `${spaces}(${mathOp}\n`;
|
|
7062
|
+
this.numSpaces++;
|
|
7063
|
+
_out3 += args.map(arg => this.toSource(arg)).join('');
|
|
7064
|
+
this.numSpaces--;
|
|
7065
|
+
_out3 += `${spaces})\n`;
|
|
7066
|
+
return _out3;
|
|
7067
|
+
}
|
|
7068
|
+
const clsName = this.resolveInstanceMethodClass(callee.object, methodName);
|
|
7069
|
+
if (clsName) {
|
|
7070
|
+
const pointerName = this.instancePointerName(callee.object);
|
|
7071
|
+
if (pointerName) {
|
|
7072
|
+
let _out4 = `${spaces}(call $${clsName}_${methodName}\n`;
|
|
7073
|
+
this.numSpaces++;
|
|
7074
|
+
_out4 += `${this.spaces}(local.get $${pointerName})\n`;
|
|
7075
|
+
_out4 += args.map(arg => this.toSource(arg)).join('');
|
|
7076
|
+
this.numSpaces--;
|
|
7077
|
+
_out4 += `${spaces})\n`;
|
|
7078
|
+
return _out4;
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
const funcName = callee.name;
|
|
7083
|
+
let out = `${spaces}(call $${funcName}\n`;
|
|
7084
|
+
this.numSpaces++;
|
|
7085
|
+
const argsCode = args.map(arg => this.toSource(arg)).join('');
|
|
7086
|
+
this.numSpaces--;
|
|
7087
|
+
out += argsCode;
|
|
7088
|
+
out += `${spaces})\n`;
|
|
7089
|
+
return out;
|
|
7090
|
+
}
|
|
7091
|
+
/**
|
|
7092
|
+
* Converts a NewExpression node to a $<ClassName>_new factory call.
|
|
7093
|
+
* @param {import("@babel/types").NewExpression} node - The Babel AST node.
|
|
7094
|
+
* @returns {string} WAT representation of the node.
|
|
7095
|
+
*/
|
|
7096
|
+
NewExpression(node) {
|
|
7097
|
+
const {
|
|
7098
|
+
callee,
|
|
7099
|
+
arguments: args
|
|
7100
|
+
} = node;
|
|
7101
|
+
const {
|
|
7102
|
+
spaces
|
|
7103
|
+
} = this;
|
|
7104
|
+
if (!this.classes[callee.name]) {
|
|
7105
|
+
return `${spaces}(f32.const 0.0) ;; Unsupported new ${callee.name}\n`;
|
|
7106
|
+
}
|
|
7107
|
+
let out = `${spaces}(call $${callee.name}_new\n`;
|
|
7108
|
+
this.numSpaces++;
|
|
7109
|
+
const argsCode = args.map(arg => this.toSource(arg)).join('');
|
|
7110
|
+
this.numSpaces--;
|
|
7111
|
+
out += argsCode;
|
|
7112
|
+
out += `${spaces})\n`;
|
|
7113
|
+
return out;
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
|
|
7117
|
+
/** @typedef {import('@babel/types').Node} Node */
|
|
7118
|
+
/**
|
|
7119
|
+
* Converts a Babel-TS type AST node into a JSDoc compatible type string,
|
|
7120
|
+
* e.g. `number`, `string[]`, `Map<string, number>` or `{a: number, b?: string}`.
|
|
7121
|
+
*
|
|
7122
|
+
* The produced strings are valid TypeScript types as well, so they can be fed
|
|
7123
|
+
* back into `expandType`/`parseJSDoc` of this very project.
|
|
7124
|
+
* @param {Node} node - The Babel-TS type node.
|
|
7125
|
+
* @returns {string} The JSDoc-compatible type string.
|
|
7126
|
+
*/
|
|
7127
|
+
function tsTypeToJSDoc(node) {
|
|
7128
|
+
if (!node) {
|
|
7129
|
+
return 'any';
|
|
7130
|
+
}
|
|
7131
|
+
switch (node.type) {
|
|
7132
|
+
case 'TSAnyKeyword':
|
|
7133
|
+
return 'any';
|
|
7134
|
+
case 'TSBigIntKeyword':
|
|
7135
|
+
return 'bigint';
|
|
7136
|
+
case 'TSBooleanKeyword':
|
|
7137
|
+
return 'boolean';
|
|
7138
|
+
case 'TSNeverKeyword':
|
|
7139
|
+
return 'never';
|
|
7140
|
+
case 'TSNullKeyword':
|
|
7141
|
+
return 'null';
|
|
7142
|
+
case 'TSNumberKeyword':
|
|
7143
|
+
return 'number';
|
|
7144
|
+
case 'TSObjectKeyword':
|
|
7145
|
+
return 'object';
|
|
7146
|
+
case 'TSStringKeyword':
|
|
7147
|
+
return 'string';
|
|
7148
|
+
case 'TSSymbolKeyword':
|
|
7149
|
+
return 'symbol';
|
|
7150
|
+
case 'TSUndefinedKeyword':
|
|
7151
|
+
return 'undefined';
|
|
7152
|
+
case 'TSUnknownKeyword':
|
|
7153
|
+
return 'unknown';
|
|
7154
|
+
case 'TSVoidKeyword':
|
|
7155
|
+
return 'void';
|
|
7156
|
+
case 'TSThisType':
|
|
7157
|
+
return 'this';
|
|
7158
|
+
case 'TSIntrinsicKeyword':
|
|
7159
|
+
return 'intrinsic';
|
|
7160
|
+
case 'TSParenthesizedType':
|
|
7161
|
+
return '(' + tsTypeToJSDoc(node.typeAnnotation) + ')';
|
|
7162
|
+
case 'TSLiteralType':
|
|
7163
|
+
return literalToJSDoc(node.literal);
|
|
7164
|
+
case 'TSArrayType':
|
|
7165
|
+
return tsTypeToJSDoc(node.elementType) + '[]';
|
|
7166
|
+
case 'TSTupleType':
|
|
7167
|
+
return '[' + node.elementTypes.map(tsTypeToJSDoc).join(', ') + ']';
|
|
7168
|
+
case 'TSUnionType':
|
|
7169
|
+
return node.types.map(tsTypeToJSDoc).join('|');
|
|
7170
|
+
case 'TSIntersectionType':
|
|
7171
|
+
return node.types.map(tsTypeToJSDoc).join('&');
|
|
7172
|
+
case 'TSOptionalType':
|
|
7173
|
+
return tsTypeToJSDoc(node.typeAnnotation) + '?';
|
|
7174
|
+
case 'TSRestType':
|
|
7175
|
+
return '...' + tsTypeToJSDoc(node.typeAnnotation);
|
|
7176
|
+
case 'TSNamedTupleMember':
|
|
7177
|
+
return tsTypeToJSDoc(node.elementType);
|
|
7178
|
+
case 'TSTypeReference':
|
|
7179
|
+
{
|
|
7180
|
+
var _node$typeParameters;
|
|
7181
|
+
const name = simplifyReference(node.typeName);
|
|
7182
|
+
const params = (_node$typeParameters = node.typeParameters) == null ? void 0 : _node$typeParameters.params;
|
|
7183
|
+
if (params != null && params.length) {
|
|
7184
|
+
return `${name}<${params.map(tsTypeToJSDoc).join(', ')}>`;
|
|
7185
|
+
}
|
|
7186
|
+
return name;
|
|
7187
|
+
}
|
|
7188
|
+
case 'TSQualifiedName':
|
|
7189
|
+
return `${tsTypeToJSDoc(node.left)}.${tsTypeToJSDoc(node.right)}`;
|
|
7190
|
+
case 'TSExpressionWithTypeArguments':
|
|
7191
|
+
return simplifyReference(node.expression);
|
|
7192
|
+
case 'Identifier':
|
|
7193
|
+
return node.name;
|
|
7194
|
+
case 'TSTypeAnnotation':
|
|
7195
|
+
return tsTypeToJSDoc(node.typeAnnotation);
|
|
7196
|
+
case 'TSTypeQuery':
|
|
7197
|
+
return 'typeof ' + tsTypeToJSDoc(node.exprName);
|
|
7198
|
+
case 'TSTypeOperator':
|
|
7199
|
+
return node.operator + ' ' + tsTypeToJSDoc(node.typeAnnotation);
|
|
7200
|
+
case 'TSIndexedAccessType':
|
|
7201
|
+
return tsTypeToJSDoc(node.objectType) + '[' + tsTypeToJSDoc(node.indexType) + ']';
|
|
7202
|
+
case 'TSTypeLiteral':
|
|
7203
|
+
return typeLiteralMembersToJSDoc(node.members);
|
|
7204
|
+
case 'TSPropertySignature':
|
|
7205
|
+
return propertySignatureToJSDoc(node);
|
|
7206
|
+
case 'TSIndexSignature':
|
|
7207
|
+
{
|
|
7208
|
+
const {
|
|
7209
|
+
parameters,
|
|
7210
|
+
typeAnnotation
|
|
7211
|
+
} = node;
|
|
7212
|
+
const pad = parameters.length ? `${tsTypeToJSDoc(parameters[0].typeAnnotation)}` : 'any';
|
|
7213
|
+
return `[key: ${pad}]: ${tsTypeToJSDoc(typeAnnotation.typeAnnotation)}`;
|
|
7214
|
+
}
|
|
7215
|
+
case 'TSFunctionType':
|
|
7216
|
+
return functionSignatureToJSDoc(node);
|
|
7217
|
+
case 'TSConstructorType':
|
|
7218
|
+
return `new ${functionSignatureToJSDoc(node)}`;
|
|
7219
|
+
case 'TSCallSignatureDeclaration':
|
|
7220
|
+
return functionSignatureToJSDoc(node);
|
|
7221
|
+
case 'TSConstructSignatureDeclaration':
|
|
7222
|
+
return `new ${functionSignatureToJSDoc(node)}`;
|
|
7223
|
+
case 'TSTypePredicate':
|
|
7224
|
+
return `${tsTypeToJSDoc(node.parameterName)} is ${tsTypeToJSDoc(node.typeAnnotation)}`;
|
|
7225
|
+
case 'TSImportType':
|
|
7226
|
+
{
|
|
7227
|
+
var _typeArguments$params;
|
|
7228
|
+
const {
|
|
7229
|
+
argument,
|
|
7230
|
+
qualifier,
|
|
7231
|
+
typeArguments
|
|
7232
|
+
} = node;
|
|
7233
|
+
const arg = argument.type === 'StringLiteral' ? `'${jsImportSource(argument)}'` : tsTypeToJSDoc(argument);
|
|
7234
|
+
let out = 'import(' + arg + ')';
|
|
7235
|
+
if (qualifier) {
|
|
7236
|
+
out += '.' + tsTypeToJSDoc(qualifier);
|
|
7237
|
+
}
|
|
7238
|
+
if (typeArguments != null && (_typeArguments$params = typeArguments.params) != null && _typeArguments$params.length) {
|
|
7239
|
+
out += '<' + typeArguments.params.map(tsTypeToJSDoc).join(', ') + '>';
|
|
7240
|
+
}
|
|
7241
|
+
return out;
|
|
7242
|
+
}
|
|
7243
|
+
case 'TSMappedType':
|
|
7244
|
+
{
|
|
7245
|
+
const {
|
|
7246
|
+
typeParameter,
|
|
7247
|
+
typeAnnotation
|
|
7248
|
+
} = node;
|
|
7249
|
+
const name = tsTypeToJSDoc(typeParameter == null ? void 0 : typeParameter.name);
|
|
7250
|
+
const constraint = typeParameter != null && typeParameter.constraint ? ' in ' + tsTypeToJSDoc(typeParameter.constraint) : '';
|
|
7251
|
+
const optional = node.optional ? '?' : '';
|
|
7252
|
+
const value = typeAnnotation ? tsTypeToJSDoc(typeAnnotation) : 'any';
|
|
7253
|
+
return `{ [${name}${constraint}]${optional}: ${value} }`;
|
|
7254
|
+
}
|
|
7255
|
+
case 'TSConditionalType':
|
|
7256
|
+
return `${tsTypeToJSDoc(node.checkType)} extends ${tsTypeToJSDoc(node.extendsType)} ? ${tsTypeToJSDoc(node.trueType)} : ${tsTypeToJSDoc(node.falseType)}`;
|
|
7257
|
+
case 'TSTemplateLiteralType':
|
|
7258
|
+
return templateLiteralToJSDoc(node);
|
|
7259
|
+
default:
|
|
7260
|
+
console.warn('ts2js> tsTypeToJSDoc unhandled type', node.type, node);
|
|
7261
|
+
return 'any';
|
|
7262
|
+
}
|
|
7263
|
+
}
|
|
7264
|
+
/**
|
|
7265
|
+
* Reduces a possibly namespace-qualified type name to its local identifier,
|
|
7266
|
+
* e.g. `Validation.StringValidator` becomes `StringValidator`, since flattened
|
|
7267
|
+
* namespaces hoist their interfaces/classes to plain identifiers.
|
|
7268
|
+
* @param {Node} node - The referenced name node.
|
|
7269
|
+
* @returns {string} The simplified JSDoc type string.
|
|
7270
|
+
*/
|
|
7271
|
+
function simplifyReference(node) {
|
|
7272
|
+
if ((node == null ? void 0 : node.type) === 'TSQualifiedName') {
|
|
7273
|
+
return simplifyReference(node.right);
|
|
7274
|
+
}
|
|
7275
|
+
return tsTypeToJSDoc(node);
|
|
7276
|
+
}
|
|
7277
|
+
/**
|
|
7278
|
+
* Converts a literal type node to its JSDoc representation.
|
|
7279
|
+
* @param {Node} literal - The literal node.
|
|
7280
|
+
* @returns {string} The JSDoc type string.
|
|
7281
|
+
*/
|
|
7282
|
+
function literalToJSDoc(literal) {
|
|
7283
|
+
if (literal.type === 'UnaryExpression') {
|
|
7284
|
+
return literal.operator + tsTypeToJSDoc(literal.argument);
|
|
7285
|
+
}
|
|
7286
|
+
if (literal.type === 'StringLiteral' || literal.type === 'NumericLiteral' || literal.type === 'BooleanLiteral') {
|
|
7287
|
+
var _literal$extra$raw, _literal$extra;
|
|
7288
|
+
return (_literal$extra$raw = (_literal$extra = literal.extra) == null ? void 0 : _literal$extra.raw) != null ? _literal$extra$raw : String(literal.value);
|
|
7289
|
+
}
|
|
7290
|
+
if (literal.type === 'BigIntLiteral') {
|
|
7291
|
+
var _literal$extra$raw2, _literal$extra2;
|
|
7292
|
+
return (_literal$extra$raw2 = (_literal$extra2 = literal.extra) == null ? void 0 : _literal$extra2.raw) != null ? _literal$extra$raw2 : literal.value;
|
|
7293
|
+
}
|
|
7294
|
+
return tsTypeToJSDoc(literal);
|
|
7295
|
+
}
|
|
7296
|
+
/**
|
|
7297
|
+
* Converts a `TSTypeLiteral`'s members into a JSDoc object type string,
|
|
7298
|
+
* e.g. `{a: number, b?: string}`.
|
|
7299
|
+
* @param {Node[]} members - The members of the type literal.
|
|
7300
|
+
* @returns {string} The JSDoc object type string.
|
|
7301
|
+
*/
|
|
7302
|
+
function typeLiteralMembersToJSDoc(members) {
|
|
7303
|
+
const props = members.map(tsTypeToJSDoc);
|
|
7304
|
+
return '{' + props.join(', ') + '}';
|
|
7305
|
+
}
|
|
7306
|
+
/**
|
|
7307
|
+
* @param {Node} node - The `TSPropertySignature` node.
|
|
7308
|
+
* @returns {string} The `name: type` (or optional `name?: type`) string.
|
|
7309
|
+
*/
|
|
7310
|
+
function propertySignatureToJSDoc(node) {
|
|
7311
|
+
const key = tsTypeToJSDoc(node.key);
|
|
7312
|
+
const optional = node.optional ? '?' : '';
|
|
7313
|
+
const type = node.typeAnnotation ? tsTypeToJSDoc(node.typeAnnotation.typeAnnotation) : 'any';
|
|
7314
|
+
return `${key}${optional}: ${type}`;
|
|
7315
|
+
}
|
|
7316
|
+
/**
|
|
7317
|
+
* @param {Node} node - A function-like type node (`TSFunctionType`, `TSCallSignatureDeclaration`, ...).
|
|
7318
|
+
* @returns {string} The `(a: A) => R` style JSDoc type string.
|
|
7319
|
+
*/
|
|
7320
|
+
function functionSignatureToJSDoc(node) {
|
|
7321
|
+
const params = node.parameters.map(param => {
|
|
7322
|
+
const name = param.type === 'Identifier' ? param.name : '';
|
|
7323
|
+
const type = param.typeAnnotation ? tsTypeToJSDoc(param.typeAnnotation.typeAnnotation) : 'any';
|
|
7324
|
+
return name && name !== 'this' ? `${name}: ${type}` : type;
|
|
7325
|
+
}).join(', ');
|
|
7326
|
+
const retType = node.typeAnnotation ? tsTypeToJSDoc(node.typeAnnotation.typeAnnotation) : 'void';
|
|
7327
|
+
return `(${params}) => ${retType}`;
|
|
7328
|
+
}
|
|
7329
|
+
/**
|
|
7330
|
+
* @param {Node} node - The `TSTemplateLiteralType` node.
|
|
7331
|
+
* @returns {string} The template literal type as string.
|
|
7332
|
+
*/
|
|
7333
|
+
function templateLiteralToJSDoc(node) {
|
|
7334
|
+
const {
|
|
7335
|
+
quasis,
|
|
7336
|
+
types
|
|
7337
|
+
} = node;
|
|
7338
|
+
let out = '`';
|
|
7339
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
7340
|
+
out += quasis[i].value.raw;
|
|
7341
|
+
if (types[i]) {
|
|
7342
|
+
out += '${' + tsTypeToJSDoc(types[i]) + '}';
|
|
7343
|
+
}
|
|
7344
|
+
}
|
|
7345
|
+
return out + '`';
|
|
7346
|
+
}
|
|
7347
|
+
/**
|
|
7348
|
+
* Extracts param information and produces a `@param` JSDoc line,
|
|
7349
|
+
* or nothing when nothing can be said about the parameter.
|
|
7350
|
+
* @param {Node} param - The parameter node.
|
|
7351
|
+
* @param {number} index - The index of the parameter.
|
|
7352
|
+
* @returns {string|undefined} The `@param` line.
|
|
7353
|
+
*/
|
|
7354
|
+
function paramToJSDoc(param, index) {
|
|
7355
|
+
if (param.type === 'TSParameterProperty') {
|
|
7356
|
+
param = param.parameter;
|
|
7357
|
+
}
|
|
7358
|
+
let type;
|
|
7359
|
+
let name;
|
|
7360
|
+
let optional = false;
|
|
7361
|
+
let rest = false;
|
|
7362
|
+
let defaultText;
|
|
7363
|
+
const typeAnnotationOf = value => {
|
|
7364
|
+
var _value$typeAnnotation;
|
|
7365
|
+
return value == null || (_value$typeAnnotation = value.typeAnnotation) == null ? void 0 : _value$typeAnnotation.typeAnnotation;
|
|
7366
|
+
};
|
|
7367
|
+
if (param.type === 'Identifier') {
|
|
7368
|
+
name = param.name;
|
|
7369
|
+
optional = param.optional;
|
|
7370
|
+
type = typeAnnotationOf(param);
|
|
7371
|
+
} else if (param.type === 'AssignmentPattern') {
|
|
7372
|
+
var _param$right, _param$right2, _param$right3;
|
|
7373
|
+
optional = true;
|
|
7374
|
+
const left = param.left;
|
|
7375
|
+
if (left.type === 'Identifier') {
|
|
7376
|
+
name = left.name;
|
|
7377
|
+
type = typeAnnotationOf(left);
|
|
7378
|
+
} else {
|
|
7379
|
+
var _left$id;
|
|
7380
|
+
name = 'param' + index;
|
|
7381
|
+
if (((_left$id = left.id) == null ? void 0 : _left$id.type) === 'Identifier') {
|
|
7382
|
+
name = left.id.name;
|
|
7383
|
+
}
|
|
7384
|
+
type = typeAnnotationOf(left);
|
|
7385
|
+
}
|
|
7386
|
+
if (((_param$right = param.right) == null ? void 0 : _param$right.type) === 'StringLiteral' || ((_param$right2 = param.right) == null ? void 0 : _param$right2.type) === 'NumericLiteral' || ((_param$right3 = param.right) == null ? void 0 : _param$right3.type) === 'BooleanLiteral') {
|
|
7387
|
+
var _param$right$extra$ra, _param$right$extra;
|
|
7388
|
+
defaultText = (_param$right$extra$ra = (_param$right$extra = param.right.extra) == null ? void 0 : _param$right$extra.raw) != null ? _param$right$extra$ra : String(param.right.value);
|
|
7389
|
+
}
|
|
7390
|
+
if (!type) {
|
|
7391
|
+
type = inferTypeFromDefault(param.right);
|
|
7392
|
+
}
|
|
7393
|
+
} else if (param.type === 'RestElement') {
|
|
7394
|
+
rest = true;
|
|
7395
|
+
const argument = param.argument;
|
|
7396
|
+
if (argument.type === 'Identifier') {
|
|
7397
|
+
name = argument.name;
|
|
7398
|
+
type = typeAnnotationOf(param);
|
|
7399
|
+
} else {
|
|
7400
|
+
type = typeAnnotationOf(param);
|
|
7401
|
+
name = 'param' + index;
|
|
7402
|
+
}
|
|
7403
|
+
} else {
|
|
7404
|
+
// ObjectPattern / ArrayPattern
|
|
7405
|
+
name = 'param' + index;
|
|
7406
|
+
type = typeAnnotationOf(param);
|
|
7407
|
+
}
|
|
7408
|
+
let nameStr = name || 'param' + index;
|
|
7409
|
+
if (optional) {
|
|
7410
|
+
nameStr = '[' + nameStr + (defaultText !== undefined ? ' = ' + defaultText : '') + ']';
|
|
7411
|
+
}
|
|
7412
|
+
if (!type) {
|
|
7413
|
+
return `@param {any} ${nameStr}`;
|
|
7414
|
+
}
|
|
7415
|
+
const typeStr = rest ? '...' + restElementType(tsTypeToJSDoc(type)) : tsTypeToJSDoc(type);
|
|
7416
|
+
return `@param {${typeStr}} ${nameStr}`;
|
|
7417
|
+
}
|
|
7418
|
+
/**
|
|
7419
|
+
* Unwraps the trailing `[]` of an array type in a rest parameter context,
|
|
7420
|
+
* e.g. `@param {...string[]} rest` becomes `@param {...string} rest`.
|
|
7421
|
+
* @param {string} typeStr - The JSDoc type string.
|
|
7422
|
+
* @returns {string} The element type.
|
|
7423
|
+
*/
|
|
7424
|
+
function restElementType(typeStr) {
|
|
7425
|
+
if (typeStr.endsWith('[]')) {
|
|
7426
|
+
return typeStr.slice(0, -2);
|
|
7427
|
+
}
|
|
7428
|
+
return typeStr;
|
|
7429
|
+
}
|
|
7430
|
+
/**
|
|
7431
|
+
* Infers a JSDoc type from a default value literal.
|
|
7432
|
+
* @param {Node} node - The default value node.
|
|
7433
|
+
* @returns {import('@babel/types').Node|undefined} A primitive keyword node or `undefined`.
|
|
7434
|
+
*/
|
|
7435
|
+
function inferTypeFromDefault(node) {
|
|
7436
|
+
let keyword;
|
|
7437
|
+
switch (node == null ? void 0 : node.type) {
|
|
7438
|
+
case 'StringLiteral':
|
|
7439
|
+
keyword = 'TSStringKeyword';
|
|
7440
|
+
break;
|
|
7441
|
+
case 'NumericLiteral':
|
|
7442
|
+
keyword = 'TSNumberKeyword';
|
|
7443
|
+
break;
|
|
7444
|
+
case 'BooleanLiteral':
|
|
7445
|
+
keyword = 'TSBooleanKeyword';
|
|
7446
|
+
break;
|
|
7447
|
+
case 'NullLiteral':
|
|
7448
|
+
keyword = 'TSNullKeyword';
|
|
7449
|
+
break;
|
|
7450
|
+
case 'ArrayExpression':
|
|
7451
|
+
keyword = 'TSArrayType';
|
|
7452
|
+
break;
|
|
7453
|
+
default:
|
|
7454
|
+
return undefined;
|
|
7455
|
+
}
|
|
7456
|
+
return {
|
|
7457
|
+
type: keyword
|
|
7458
|
+
};
|
|
7459
|
+
}
|
|
7460
|
+
/**
|
|
7461
|
+
* @param {Node} node - The function-like node.
|
|
7462
|
+
* @returns {boolean} True if any parameter carries a type annotation.
|
|
7463
|
+
*/
|
|
7464
|
+
function hasTypedParams(node) {
|
|
7465
|
+
return node.params.some(param => {
|
|
7466
|
+
var _param$left, _param$argument;
|
|
7467
|
+
if (param.type === 'TSParameterProperty') {
|
|
7468
|
+
param = param.parameter;
|
|
7469
|
+
}
|
|
7470
|
+
return param.typeAnnotation || ((_param$left = param.left) == null ? void 0 : _param$left.typeAnnotation) || ((_param$argument = param.argument) == null ? void 0 : _param$argument.typeAnnotation);
|
|
7471
|
+
});
|
|
7472
|
+
}
|
|
7473
|
+
/**
|
|
7474
|
+
* Returns the JSDoc lines for a function-like node.
|
|
7475
|
+
* @param {Node} node - The function-like node.
|
|
7476
|
+
* @returns {string[]} The JSDoc lines (without `@param`/`@returns` separators).
|
|
7477
|
+
*/
|
|
7478
|
+
function jsdocLinesFromFunction(node) {
|
|
7479
|
+
var _node$typeParameters2, _node$returnType;
|
|
7480
|
+
const lines = [];
|
|
7481
|
+
if ((_node$typeParameters2 = node.typeParameters) != null && _node$typeParameters2.params) {
|
|
7482
|
+
for (const tp of node.typeParameters.params) {
|
|
7483
|
+
const name = tp.name;
|
|
7484
|
+
lines.push(`@template {${name}} ${name}`);
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
const addParams = node.type !== 'TSDeclareFunction' && node.kind !== 'get';
|
|
7488
|
+
if (addParams && hasTypedParams(node)) {
|
|
7489
|
+
node.params.forEach((param, i) => lines.push(paramToJSDoc(param, i)));
|
|
7490
|
+
}
|
|
7491
|
+
if ((_node$returnType = node.returnType) != null && _node$returnType.typeAnnotation) {
|
|
7492
|
+
lines.push(`@returns {${tsTypeToJSDoc(node.returnType.typeAnnotation)}}`);
|
|
7493
|
+
}
|
|
7494
|
+
return lines;
|
|
7495
|
+
}
|
|
7496
|
+
/**
|
|
7497
|
+
* Injects `this.prop = prop;` statements for constructor parameter properties
|
|
7498
|
+
* (`constructor(public prop: number)`).
|
|
7499
|
+
* @param {import('@babel/types').ClassMethod|import('@babel/types').ClassPrivateMethod} node - The constructor node.
|
|
7500
|
+
*/
|
|
7501
|
+
function injectParameterProperties(node) {
|
|
7502
|
+
var _node$body;
|
|
7503
|
+
const body = (_node$body = node.body) == null ? void 0 : _node$body.body;
|
|
7504
|
+
if (!Array.isArray(body)) {
|
|
7505
|
+
return;
|
|
7506
|
+
}
|
|
7507
|
+
const properties = node.params.filter(param => param.type === 'TSParameterProperty');
|
|
7508
|
+
for (const property of properties) {
|
|
7509
|
+
const parameter = property.parameter;
|
|
7510
|
+
if (parameter.type !== 'Identifier' && parameter.type !== 'AssignmentPattern') {
|
|
7511
|
+
console.warn('ts2js> unhandled parameter property', property);
|
|
7512
|
+
continue;
|
|
7513
|
+
}
|
|
7514
|
+
const target = parameter.type === 'AssignmentPattern' ? parameter.left : parameter;
|
|
7515
|
+
if (target.type !== 'Identifier') {
|
|
7516
|
+
console.warn('ts2js> unhandled parameter property target', property);
|
|
7517
|
+
continue;
|
|
7518
|
+
}
|
|
7519
|
+
const {
|
|
7520
|
+
name
|
|
7521
|
+
} = target;
|
|
7522
|
+
const alreadyAssigned = body.some(stmt => {
|
|
7523
|
+
if (stmt.type !== 'ExpressionStatement') return false;
|
|
7524
|
+
const expr = stmt.expression;
|
|
7525
|
+
return expr.type === 'AssignmentExpression' && expr.operator === '=' && expr.left.type === 'MemberExpression' && expr.left.object.type === 'ThisExpression' && expr.left.property.type === 'Identifier' && expr.left.property.name === name;
|
|
7526
|
+
});
|
|
7527
|
+
if (alreadyAssigned) {
|
|
7528
|
+
continue;
|
|
7529
|
+
}
|
|
7530
|
+
const rightNode = target;
|
|
7531
|
+
const assignment = {
|
|
7532
|
+
type: 'ExpressionStatement',
|
|
7533
|
+
expression: {
|
|
7534
|
+
type: 'AssignmentExpression',
|
|
7535
|
+
operator: '=',
|
|
7536
|
+
left: {
|
|
7537
|
+
type: 'MemberExpression',
|
|
7538
|
+
object: {
|
|
7539
|
+
type: 'ThisExpression'
|
|
7540
|
+
},
|
|
7541
|
+
property: {
|
|
7542
|
+
type: 'Identifier',
|
|
7543
|
+
name
|
|
7544
|
+
},
|
|
7545
|
+
computed: false,
|
|
7546
|
+
optional: null
|
|
7547
|
+
},
|
|
7548
|
+
right: rightNode
|
|
7549
|
+
}
|
|
7550
|
+
};
|
|
7551
|
+
body.unshift(assignment);
|
|
7552
|
+
}
|
|
7553
|
+
}
|
|
7554
|
+
/**
|
|
7555
|
+
* A Stringifier subclass which strips TypeScript-only syntax while emitting the
|
|
7556
|
+
* JSDoc comments that were attached during the `annotate` pre-pass.
|
|
7557
|
+
*/
|
|
7558
|
+
class ToJS extends Stringifier {
|
|
7559
|
+
// --- Unwrap type-only expressions ---
|
|
7560
|
+
TSAsExpression(node) {
|
|
7561
|
+
return this.toSource(node.expression);
|
|
7562
|
+
}
|
|
7563
|
+
TSNonNullExpression(node) {
|
|
7564
|
+
return this.toSource(node.expression);
|
|
7565
|
+
}
|
|
7566
|
+
TSSatisfiesExpression(node) {
|
|
7567
|
+
return this.toSource(node.expression);
|
|
7568
|
+
}
|
|
7569
|
+
TSTypeAssertion(node) {
|
|
7570
|
+
return this.toSource(node.expression);
|
|
7571
|
+
}
|
|
7572
|
+
TSInstantiationExpression(node) {
|
|
7573
|
+
return this.toSource(node.expression);
|
|
7574
|
+
}
|
|
7575
|
+
TSTypeCastExpression(node) {
|
|
7576
|
+
return this.toSource(node.expression);
|
|
7577
|
+
}
|
|
7578
|
+
TSParameterProperty(node) {
|
|
7579
|
+
return this.toSource(node.parameter);
|
|
7580
|
+
}
|
|
7581
|
+
TSExpressionWithTypeArguments(node) {
|
|
7582
|
+
return this.toSource(node.expression);
|
|
7583
|
+
}
|
|
7584
|
+
TSImportEqualsDeclaration(node) {
|
|
7585
|
+
var _node$moduleReference;
|
|
7586
|
+
if (((_node$moduleReference = node.moduleReference) == null ? void 0 : _node$moduleReference.type) !== 'TSExternalModuleReference') {
|
|
7587
|
+
return '';
|
|
7588
|
+
}
|
|
7589
|
+
const name = this.toSource(node.id);
|
|
7590
|
+
const raw = jsImportSource(node.moduleReference.expression);
|
|
7591
|
+
if (node.importKind === 'type') {
|
|
7592
|
+
return `/** @import * as ${name} from '${raw}' */`;
|
|
7593
|
+
}
|
|
7594
|
+
return `import * as ${name} from '${raw}';`;
|
|
7595
|
+
}
|
|
7596
|
+
TSExportAssignment() {
|
|
7597
|
+
return '';
|
|
7598
|
+
}
|
|
7599
|
+
TSUndefinedKeyword() {
|
|
7600
|
+
return '';
|
|
7601
|
+
}
|
|
7602
|
+
// --- Drop type-only declarations, but keep interfaces/aliases as @typedef ---
|
|
7603
|
+
/**
|
|
7604
|
+
* @param {import('@babel/types').TSInterfaceDeclaration} node - The Babel AST node.
|
|
7605
|
+
* @returns {string} A JSDoc typedef comment.
|
|
7606
|
+
*/
|
|
7607
|
+
TSInterfaceDeclaration(node) {
|
|
7608
|
+
return this.typedefComment(node);
|
|
7609
|
+
}
|
|
7610
|
+
/**
|
|
7611
|
+
* @param {import('@babel/types').TSTypeAliasDeclaration} node - The Babel AST node.
|
|
7612
|
+
* @returns {string} A JSDoc typedef comment.
|
|
7613
|
+
*/
|
|
7614
|
+
TSTypeAliasDeclaration(node) {
|
|
7615
|
+
return this.typedefComment(node);
|
|
7616
|
+
}
|
|
7617
|
+
/**
|
|
7618
|
+
* Drop ambient declarations, they have no runtime representation.
|
|
7619
|
+
* @returns {string} An empty string.
|
|
7620
|
+
*/
|
|
7621
|
+
TSDeclareFunction() {
|
|
7622
|
+
return '';
|
|
7623
|
+
}
|
|
7624
|
+
/**
|
|
7625
|
+
* Converts a namespace (`namespace X { ... }`) into the classic IIFE
|
|
7626
|
+
* pattern, exporting members via `X.member = member` assignments.
|
|
7627
|
+
* @param {import('@babel/types').TSModuleDeclaration} node - The namespace declaration.
|
|
7628
|
+
* @returns {string} The IIFE source.
|
|
7629
|
+
*/
|
|
7630
|
+
TSModuleDeclaration(node) {
|
|
7631
|
+
var _this$namespaceLevel;
|
|
7632
|
+
const {
|
|
7633
|
+
id,
|
|
7634
|
+
body
|
|
7635
|
+
} = node;
|
|
7636
|
+
const name = (id == null ? void 0 : id.type) === 'Identifier' ? id.name : '';
|
|
7637
|
+
if (!name || node.declare || node.global || !body || body.type !== 'TSModuleBlock') {
|
|
7638
|
+
return '';
|
|
7639
|
+
}
|
|
7640
|
+
const saved = this.numSpaces;
|
|
7641
|
+
const level = (_this$namespaceLevel = this.namespaceLevel) != null ? _this$namespaceLevel : 0;
|
|
7642
|
+
this.namespaceLevel = level + 1;
|
|
7643
|
+
this.numSpaces = 0;
|
|
7644
|
+
const memberSource = this.namespaceMembers(body, name);
|
|
7645
|
+
this.numSpaces = saved;
|
|
7646
|
+
this.namespaceLevel = level;
|
|
7647
|
+
const spaces = this.spaces;
|
|
7648
|
+
const outer = ' '.repeat(level + 1);
|
|
7649
|
+
const inner = memberSource.replace(/ \*\/ +(?=[a-zA-Z_$])/g, '*/\n').split('\n').map(line => line.trim() ? outer + line.trimEnd() : line).join('\n');
|
|
7650
|
+
let out = spaces + `var ${name};\n`;
|
|
7651
|
+
out += spaces + `(function (${name}) {\n`;
|
|
7652
|
+
out += inner;
|
|
7653
|
+
out += '\n' + spaces + `})(${name} || (${name} = {}));`;
|
|
7654
|
+
return out;
|
|
7655
|
+
}
|
|
7656
|
+
/**
|
|
7657
|
+
* Renders the statements of a namespace body at base indentation, emitting
|
|
7658
|
+
* the local members and the trailing `Name.member = member;` assignments
|
|
7659
|
+
* for exported value members.
|
|
7660
|
+
* @param {import('@babel/types').TSModuleBlock} block - The namespace body.
|
|
7661
|
+
* @param {string} name - The namespace identifier.
|
|
7662
|
+
* @returns {string} The body source (joinable lines).
|
|
7663
|
+
*/
|
|
7664
|
+
namespaceMembers(block, name) {
|
|
7665
|
+
const lines = [];
|
|
7666
|
+
const assignments = [];
|
|
7667
|
+
for (const statement of block.body) {
|
|
7668
|
+
if (statement.type === 'ExportNamedDeclaration') {
|
|
7669
|
+
const {
|
|
7670
|
+
code,
|
|
7671
|
+
names
|
|
7672
|
+
} = this.namespaceExport(statement);
|
|
7673
|
+
if (code) {
|
|
7674
|
+
lines.push(code.trimEnd());
|
|
7675
|
+
}
|
|
7676
|
+
for (const member of names) {
|
|
7677
|
+
assignments.push(`${name}.${member} = ${member};`);
|
|
7678
|
+
}
|
|
7679
|
+
} else {
|
|
7680
|
+
lines.push(this.toSource(statement).trimEnd());
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
return lines.concat(assignments).join('\n');
|
|
7684
|
+
}
|
|
7685
|
+
/**
|
|
7686
|
+
* Renders an `export` statement inside a namespace: the declaration without
|
|
7687
|
+
* the `export` keyword and the list of exported value names.
|
|
7688
|
+
* @param {import('@babel/types').ExportNamedDeclaration} statement - The export statement.
|
|
7689
|
+
* @returns {{code: string, names: string[]}} The declaration source and exported names.
|
|
7690
|
+
*/
|
|
7691
|
+
namespaceExport(statement) {
|
|
7692
|
+
var _declaration$id;
|
|
7693
|
+
const {
|
|
7694
|
+
declaration
|
|
7695
|
+
} = statement;
|
|
7696
|
+
if (!declaration) {
|
|
7697
|
+
return {
|
|
7698
|
+
code: '',
|
|
7699
|
+
names: []
|
|
7700
|
+
};
|
|
7701
|
+
}
|
|
7702
|
+
let names = [];
|
|
7703
|
+
switch (declaration.type) {
|
|
7704
|
+
case 'ClassDeclaration':
|
|
7705
|
+
case 'FunctionDeclaration':
|
|
7706
|
+
case 'TSEnumDeclaration':
|
|
7707
|
+
if (declaration.id) {
|
|
7708
|
+
names = [declaration.id.name];
|
|
7709
|
+
}
|
|
7710
|
+
break;
|
|
7711
|
+
case 'VariableDeclaration':
|
|
7712
|
+
names = declaration.declarations.map(declarator => declarator.id).filter(id => id && id.type === 'Identifier').map(id => id.name);
|
|
7713
|
+
break;
|
|
7714
|
+
case 'TSModuleDeclaration':
|
|
7715
|
+
if ((_declaration$id = declaration.id) != null && _declaration$id.name) {
|
|
7716
|
+
names = [declaration.id.name];
|
|
7717
|
+
}
|
|
7718
|
+
break;
|
|
7719
|
+
}
|
|
7720
|
+
return {
|
|
7721
|
+
code: this.toSource(declaration),
|
|
7722
|
+
names
|
|
7723
|
+
};
|
|
7724
|
+
}
|
|
7725
|
+
/**
|
|
7726
|
+
* Converts `enum` into a plain `const` object.
|
|
7727
|
+
* @param {import('@babel/types').TSEnumDeclaration} node - The Babel AST node.
|
|
7728
|
+
* @returns {string} The object literal.
|
|
7729
|
+
*/
|
|
7730
|
+
TSEnumDeclaration(node) {
|
|
7731
|
+
const spaces = this.spaces;
|
|
7732
|
+
let running = 0;
|
|
7733
|
+
const entries = node.members.map(member => {
|
|
7734
|
+
const key = this.toSource(member.id);
|
|
7735
|
+
let value;
|
|
7736
|
+
if (member.initializer) {
|
|
7737
|
+
value = this.toSource(member.initializer);
|
|
7738
|
+
if (member.initializer.type === 'NumericLiteral') {
|
|
7739
|
+
running = member.initializer.value + 1;
|
|
7740
|
+
} else if (member.initializer.type === 'UnaryExpression') {
|
|
7741
|
+
var _member$initializer$e;
|
|
7742
|
+
const raw = (_member$initializer$e = member.initializer.extra) == null ? void 0 : _member$initializer$e.raw;
|
|
7743
|
+
if (raw) {
|
|
7744
|
+
const n = Number(raw);
|
|
7745
|
+
running = Number.isNaN(n) ? running : n + 1;
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
} else {
|
|
7749
|
+
value = String(running);
|
|
7750
|
+
running++;
|
|
7751
|
+
}
|
|
7752
|
+
return `${key}: ${value}`;
|
|
7753
|
+
});
|
|
7754
|
+
let out = spaces + 'const ' + this.toSource(node.id) + ' = {\n';
|
|
7755
|
+
this.numSpaces++;
|
|
7756
|
+
const innerSpaces = this.spaces;
|
|
7757
|
+
out += entries.map(_ => innerSpaces + _).join(',\n');
|
|
7758
|
+
this.numSpaces--;
|
|
7759
|
+
out += '\n' + spaces + '};';
|
|
7760
|
+
return out;
|
|
7761
|
+
}
|
|
7762
|
+
/**
|
|
7763
|
+
* Renders a typedef comment for interfaces/type aliases.
|
|
7764
|
+
* @param {Node} node - The declaration node.
|
|
7765
|
+
* @returns {string} The comment.
|
|
7766
|
+
*/
|
|
7767
|
+
typedefComment(node) {
|
|
7768
|
+
const lines = this.typedefLines(node);
|
|
7769
|
+
return this.jsdocComment(lines);
|
|
7770
|
+
}
|
|
7771
|
+
/**
|
|
7772
|
+
* Builds `@typedef` JSDoc lines for a declaration node.
|
|
7773
|
+
* @param {Node} node - The declaration node.
|
|
7774
|
+
* @returns {string[]} The JSDoc lines.
|
|
7775
|
+
*/
|
|
7776
|
+
typedefLines(node) {
|
|
7777
|
+
if (node.type === 'TSTypeAliasDeclaration') {
|
|
7778
|
+
const _name = node.id.name;
|
|
7779
|
+
return [`@typedef {${tsTypeToJSDoc(node.typeAnnotation)}} ${_name}`];
|
|
7780
|
+
}
|
|
7781
|
+
const name = node.id.name;
|
|
7782
|
+
const lines = [`@typedef {Object} ${name}`];
|
|
7783
|
+
for (const member of (_node$body$body = (_node$body2 = node.body) == null ? void 0 : _node$body2.body) != null ? _node$body$body : []) {
|
|
7784
|
+
var _node$body$body, _node$body2;
|
|
7785
|
+
if (member.type === 'TSPropertySignature') {
|
|
7786
|
+
const propName = tsTypeToJSDoc(member.key);
|
|
7787
|
+
const optional = member.optional;
|
|
7788
|
+
const type = member.typeAnnotation ? tsTypeToJSDoc(member.typeAnnotation.typeAnnotation) : 'any';
|
|
7789
|
+
lines.push(`@property {${type}} ${optional ? '[' + propName + ']' : propName}`);
|
|
7790
|
+
} else if (member.type === 'TSMethodSignature') {
|
|
7791
|
+
const methodName = tsTypeToJSDoc(member.key);
|
|
7792
|
+
const optional = member.optional;
|
|
7793
|
+
const type = functionSignatureToJSDoc(member);
|
|
7794
|
+
lines.push(`@property {${type}} ${optional ? '[' + methodName + ']' : methodName}`);
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
return lines;
|
|
7798
|
+
}
|
|
7799
|
+
/**
|
|
7800
|
+
* Builds a JSDoc comment block string with proper indentation.
|
|
7801
|
+
* @param {string[]} lines - The comment body lines (e.g. `@param {number} a`).
|
|
7802
|
+
* @returns {string} The comment string.
|
|
7803
|
+
*/
|
|
7804
|
+
jsdocComment(lines) {
|
|
7805
|
+
const spaces = this.spaces;
|
|
7806
|
+
let out = spaces + '/**\n';
|
|
7807
|
+
for (const line of lines) {
|
|
7808
|
+
out += spaces + ' * ' + line + '\n';
|
|
7809
|
+
}
|
|
7810
|
+
out += spaces + ' */';
|
|
7811
|
+
return out;
|
|
7812
|
+
}
|
|
7813
|
+
/**
|
|
7814
|
+
* Converts type-only imports into `@import` JSDoc comments and keeps
|
|
7815
|
+
* the value imports of mixed imports (`import {Value, type AlsoType}`).
|
|
7816
|
+
* @override
|
|
7817
|
+
* @param {import('@babel/types').ImportDeclaration} node - The Babel AST node.
|
|
7818
|
+
* @returns {string} Stringification of the node.
|
|
7819
|
+
*/
|
|
7820
|
+
ImportDeclaration(node) {
|
|
7821
|
+
const typeSpecifiers = node.specifiers.filter(_ => _.importKind === 'type');
|
|
7822
|
+
const valueSpecifiers = node.specifiers.filter(_ => _.importKind !== 'type');
|
|
7823
|
+
const fullyTypeOnly = node.importKind === 'type' || typeSpecifiers.length && valueSpecifiers.length === 0;
|
|
7824
|
+
if (fullyTypeOnly) {
|
|
7825
|
+
return importTypeToJSDoc(node.specifiers, node.source);
|
|
7826
|
+
}
|
|
7827
|
+
let out = '';
|
|
7828
|
+
if (typeSpecifiers.length) {
|
|
7829
|
+
out = importTypeToJSDoc(typeSpecifiers, node.source);
|
|
7830
|
+
}
|
|
7831
|
+
if (valueSpecifiers.length) {
|
|
7832
|
+
const valueImport = super.ImportDeclaration(_extends({}, node, {
|
|
7833
|
+
importKind: 'value',
|
|
7834
|
+
specifiers: valueSpecifiers
|
|
7835
|
+
}));
|
|
7836
|
+
out = out ? out + '\n' + valueImport : valueImport;
|
|
7837
|
+
}
|
|
7838
|
+
return out;
|
|
7839
|
+
}
|
|
7840
|
+
/**
|
|
7841
|
+
* Strips type-only exports and converts type declarations into typedef comments.
|
|
7842
|
+
* @override
|
|
7843
|
+
* @param {import('@babel/types').ExportNamedDeclaration} node - The Babel AST node.
|
|
7844
|
+
* @returns {string} Stringification of the node.
|
|
7845
|
+
*/
|
|
7846
|
+
ExportNamedDeclaration(node) {
|
|
7847
|
+
if (node.exportKind === 'type') {
|
|
7848
|
+
if (node.declaration) {
|
|
7849
|
+
return this.toSource(node.declaration);
|
|
7850
|
+
}
|
|
7851
|
+
return '';
|
|
7852
|
+
}
|
|
7853
|
+
if (node.specifiers.some(_ => _.exportKind === 'type')) {
|
|
7854
|
+
const specifiers = node.specifiers.filter(_ => _.exportKind !== 'type');
|
|
7855
|
+
if (specifiers.length === 0) {
|
|
7856
|
+
return '';
|
|
7857
|
+
}
|
|
7858
|
+
return super.ExportNamedDeclaration(_extends({}, node, {
|
|
7859
|
+
specifiers
|
|
7860
|
+
}));
|
|
7861
|
+
}
|
|
7862
|
+
return super.ExportNamedDeclaration(node);
|
|
7863
|
+
}
|
|
7864
|
+
/**
|
|
7865
|
+
* Strips the `declare` modifier from variable declarations.
|
|
7866
|
+
* @override
|
|
7867
|
+
* @param {import('@babel/types').VariableDeclaration} node - The Babel AST node.
|
|
7868
|
+
* @returns {string} Stringification of the node.
|
|
7869
|
+
*/
|
|
7870
|
+
VariableDeclaration(node) {
|
|
7871
|
+
if (node.declare) {
|
|
7872
|
+
return '';
|
|
7873
|
+
}
|
|
7874
|
+
return super.VariableDeclaration(_extends({}, node, {
|
|
7875
|
+
declare: false
|
|
7876
|
+
}));
|
|
7877
|
+
}
|
|
7878
|
+
}
|
|
7879
|
+
/**
|
|
7880
|
+
* Renders a JSDoc `@import` comment for type-only import specifiers,
|
|
7881
|
+
* e.g. `import type {OnlyType} from './types'` becomes the single line
|
|
7882
|
+
* `@import { OnlyType } from './types.js'` and carries the type
|
|
7883
|
+
* information without creating a runtime import.
|
|
7884
|
+
* @param {import('@babel/types').ImportSpecifier[]|import('@babel/types').ImportDefaultSpecifier[]|import('@babel/types').ImportNamespaceSpecifier[]} specifiers - The import specifiers.
|
|
7885
|
+
* @param {import('@babel/types').StringLiteral} source - The import source.
|
|
7886
|
+
* @returns {string} The `@import` JSDoc comment (or an empty string).
|
|
7887
|
+
*/
|
|
7888
|
+
function importTypeToJSDoc(specifiers, source) {
|
|
7889
|
+
const named = [];
|
|
7890
|
+
let defaultName;
|
|
7891
|
+
let namespaceName;
|
|
7892
|
+
for (const specifier of specifiers) {
|
|
7893
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
7894
|
+
var _specifier$local$name, _specifier$local, _specifier$imported;
|
|
7895
|
+
named.push((_specifier$local$name = (_specifier$local = specifier.local) == null ? void 0 : _specifier$local.name) != null ? _specifier$local$name : (_specifier$imported = specifier.imported) == null ? void 0 : _specifier$imported.name);
|
|
7896
|
+
} else if (specifier.type === 'ImportDefaultSpecifier') {
|
|
7897
|
+
var _specifier$local2;
|
|
7898
|
+
defaultName = (_specifier$local2 = specifier.local) == null ? void 0 : _specifier$local2.name;
|
|
7899
|
+
} else if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
7900
|
+
var _specifier$local3;
|
|
7901
|
+
namespaceName = (_specifier$local3 = specifier.local) == null ? void 0 : _specifier$local3.name;
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7904
|
+
const parts = [];
|
|
7905
|
+
if (defaultName) {
|
|
7906
|
+
parts.push(defaultName);
|
|
7907
|
+
}
|
|
7908
|
+
if (named.length) {
|
|
7909
|
+
parts.push('{ ' + named.join(', ') + ' }');
|
|
7910
|
+
}
|
|
7911
|
+
if (namespaceName) {
|
|
7912
|
+
parts.push('* as ' + namespaceName);
|
|
7913
|
+
}
|
|
7914
|
+
if (!parts.length) {
|
|
7915
|
+
return '';
|
|
7916
|
+
}
|
|
7917
|
+
return `/** @import ${parts.join(', ')} from '${jsImportSource(source)}' */`;
|
|
7918
|
+
}
|
|
7919
|
+
/**
|
|
7920
|
+
* Rewrites a TypeScript import specifier to its JavaScript counterpart,
|
|
7921
|
+
* e.g. `./types` becomes `./types.js`.
|
|
7922
|
+
* @param {import('@babel/types').StringLiteral} source - The import source.
|
|
7923
|
+
* @returns {string} The JavaScript module specifier.
|
|
7924
|
+
*/
|
|
7925
|
+
function jsImportSource(source) {
|
|
7926
|
+
const raw = source.value;
|
|
7927
|
+
if (!raw.startsWith('.') && !raw.startsWith('/')) {
|
|
7928
|
+
return raw;
|
|
7929
|
+
}
|
|
7930
|
+
if (/\.(ts|tsx|mts|cts)$/.test(raw)) {
|
|
7931
|
+
return raw.replace(/\.(ts|tsx|mts|cts)$/, '.js');
|
|
7932
|
+
}
|
|
7933
|
+
if (!/\.[a-zA-Z][a-zA-Z0-9]*$/.test(raw)) {
|
|
7934
|
+
return raw + '.js';
|
|
7935
|
+
}
|
|
7936
|
+
return raw;
|
|
7937
|
+
}
|
|
7938
|
+
/**
|
|
7939
|
+
* Attaches JSDoc comment blocks to the AST nodes which carry TypeScript types.
|
|
7940
|
+
* @param {Node} node - The node to annotate recursively.
|
|
7941
|
+
* @param {Node[]} parents - The current parent stack.
|
|
7942
|
+
*/
|
|
7943
|
+
function annotate(node, parents) {
|
|
7944
|
+
if (!node || typeof node !== 'object') {
|
|
7945
|
+
return;
|
|
7946
|
+
}
|
|
7947
|
+
parents.push(node);
|
|
7948
|
+
const {
|
|
7949
|
+
type
|
|
7950
|
+
} = node;
|
|
7951
|
+
if (nodeIsFunctionLike(node)) {
|
|
7952
|
+
if ((type === 'ClassMethod' || type === 'ClassPrivateMethod') && node.kind === 'constructor') {
|
|
7953
|
+
injectParameterProperties(node);
|
|
7954
|
+
}
|
|
7955
|
+
const lines = jsdocLinesFromFunction(node);
|
|
7956
|
+
if (lines.length) {
|
|
7957
|
+
attachComment(node, lines, parents);
|
|
7958
|
+
}
|
|
7959
|
+
} else if (type === 'ClassProperty' || type === 'ClassPrivateProperty') {
|
|
7960
|
+
var _node$typeAnnotation;
|
|
7961
|
+
if ((_node$typeAnnotation = node.typeAnnotation) != null && _node$typeAnnotation.typeAnnotation && !node.declare) {
|
|
7962
|
+
attachComment(node, [`@type {${tsTypeToJSDoc(node.typeAnnotation.typeAnnotation)}}`], parents);
|
|
7963
|
+
}
|
|
7964
|
+
} else if (type === 'ClassDeclaration' || type === 'ClassExpression') {
|
|
7965
|
+
const interfaces = node.implements || [];
|
|
7966
|
+
if (interfaces != null && interfaces.length) {
|
|
7967
|
+
const lines = interfaces.map(imp => `@implements {${simplifyReference(imp.expression)}}`);
|
|
7968
|
+
attachComment(node, lines, parents);
|
|
7969
|
+
}
|
|
7970
|
+
} else if (type === 'VariableDeclaration' && !node.declare) {
|
|
7971
|
+
collectVariableTypeComments(node, parents);
|
|
7972
|
+
}
|
|
7973
|
+
// recurse into children based on the annotated keys
|
|
7974
|
+
const keys = nodeChildren[type];
|
|
7975
|
+
for (const key of keys != null ? keys : []) {
|
|
7976
|
+
const child = node[key];
|
|
7977
|
+
if (Array.isArray(child)) {
|
|
7978
|
+
for (const entry of child) {
|
|
7979
|
+
annotate(entry, parents);
|
|
7980
|
+
}
|
|
7981
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
7982
|
+
annotate(child, parents);
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
parents.pop();
|
|
7986
|
+
}
|
|
7987
|
+
/**
|
|
7988
|
+
* Attaches a JSDoc comment (built from `lines`) to a suitable statement-level host node.
|
|
7989
|
+
* @param {Node} node - The annotated node.
|
|
7990
|
+
* @param {string[]} lines - The JSDoc lines.
|
|
7991
|
+
* @param {Node[]} parents - The parent stack.
|
|
7992
|
+
*/
|
|
7993
|
+
function attachComment(node, lines, parents) {
|
|
7994
|
+
let host = findCommentHost(node, parents);
|
|
7995
|
+
if (!host) {
|
|
7996
|
+
host = node;
|
|
7997
|
+
}
|
|
7998
|
+
const comment = makeComment(node, lines, parents);
|
|
7999
|
+
host.leadingComments = [...(host.leadingComments || []), comment];
|
|
8000
|
+
}
|
|
8001
|
+
/**
|
|
8002
|
+
* Finds the statement-ish node where a JSDoc comment should live.
|
|
8003
|
+
* For `const f = (a) => {}` the comment belongs on the VariableDeclaration,
|
|
8004
|
+
* for `export function f` it belongs on the ExportNamedDeclaration.
|
|
8005
|
+
* @param {Node} node - The function/property node.
|
|
8006
|
+
* @param {Node[]} parents - The parent stack.
|
|
8007
|
+
* @returns {Node} The host node.
|
|
8008
|
+
*/
|
|
8009
|
+
function findCommentHost(node, parents) {
|
|
8010
|
+
const index = parents.findLastIndex(_ => _ === node);
|
|
8011
|
+
const parent = parents[index - 1];
|
|
8012
|
+
if ((parent == null ? void 0 : parent.type) === 'ExportNamedDeclaration') {
|
|
8013
|
+
var _parents;
|
|
8014
|
+
// Inside namespaces the export wrapper is consumed by the namespace
|
|
8015
|
+
// emitter, so the comment has to live on the declaration itself.
|
|
8016
|
+
if (((_parents = parents[index - 2]) == null ? void 0 : _parents.type) === 'TSModuleBlock') {
|
|
8017
|
+
return node;
|
|
8018
|
+
}
|
|
8019
|
+
return parent;
|
|
8020
|
+
}
|
|
8021
|
+
if ((parent == null ? void 0 : parent.type) === 'VariableDeclarator') {
|
|
8022
|
+
const grand = parents[index - 2];
|
|
8023
|
+
if ((grand == null ? void 0 : grand.type) === 'VariableDeclaration') {
|
|
8024
|
+
var _parents2;
|
|
8025
|
+
if (((_parents2 = parents[index - 3]) == null ? void 0 : _parents2.type) === 'ExportNamedDeclaration') {
|
|
8026
|
+
return parents[index - 3];
|
|
8027
|
+
}
|
|
8028
|
+
return grand;
|
|
8029
|
+
}
|
|
8030
|
+
}
|
|
8031
|
+
if ((parent == null ? void 0 : parent.type) === 'ExpressionStatement') {
|
|
8032
|
+
return parent;
|
|
8033
|
+
}
|
|
8034
|
+
// Class-methods/properties and object-methods host the comment themselves.
|
|
8035
|
+
return node;
|
|
8036
|
+
}
|
|
8037
|
+
/**
|
|
8038
|
+
* Creates a Babel CommentBlock node with a fabricated `loc`.
|
|
8039
|
+
* The column is derived from the renderer indentation depth of the annotated
|
|
8040
|
+
* node (2 spaces per indenting ancestor) so that the comment aligns with the
|
|
8041
|
+
* `spaces` of the node's rendered position, independent of the original
|
|
8042
|
+
* (namespace-shifted) source column.
|
|
8043
|
+
* @param {Node} node - The annotated node.
|
|
8044
|
+
* @param {string[]} lines - The JSDoc lines.
|
|
8045
|
+
* @param {Node[]} parents - The parent stack.
|
|
8046
|
+
* @returns {import('@babel/types').CommentBlock} The comment node.
|
|
8047
|
+
*/
|
|
8048
|
+
function makeComment(node, lines, parents) {
|
|
8049
|
+
var _ref, _node$start, _node$loc, _node$loc$start$line, _node$loc2, _node$loc$start$line2, _node$loc3;
|
|
8050
|
+
const value = '*\n * ' + lines.join('\n * ');
|
|
8051
|
+
const start = (_ref = (_node$start = node.start) != null ? _node$start : (_node$loc = node.loc) == null || (_node$loc = _node$loc.start) == null ? void 0 : _node$loc.index) != null ? _ref : 0;
|
|
8052
|
+
const startColumn = 2 * renderIndentDepth(node, parents);
|
|
8053
|
+
return {
|
|
8054
|
+
type: 'CommentBlock',
|
|
8055
|
+
value,
|
|
8056
|
+
loc: {
|
|
8057
|
+
start: {
|
|
8058
|
+
index: start,
|
|
8059
|
+
line: (_node$loc$start$line = (_node$loc2 = node.loc) == null || (_node$loc2 = _node$loc2.start) == null ? void 0 : _node$loc2.line) != null ? _node$loc$start$line : 1,
|
|
8060
|
+
column: startColumn
|
|
8061
|
+
},
|
|
8062
|
+
end: {
|
|
8063
|
+
index: start,
|
|
8064
|
+
line: (_node$loc$start$line2 = (_node$loc3 = node.loc) == null || (_node$loc3 = _node$loc3.start) == null ? void 0 : _node$loc3.line) != null ? _node$loc$start$line2 : 1,
|
|
8065
|
+
column: startColumn
|
|
8066
|
+
}
|
|
8067
|
+
}
|
|
8068
|
+
};
|
|
8069
|
+
}
|
|
8070
|
+
/**
|
|
8071
|
+
* Node types whose renderer increases the indentation level by one (`this.numSpaces++`).
|
|
8072
|
+
* @type {Set<string>}
|
|
8073
|
+
*/
|
|
8074
|
+
const indentingNodeTypes = new Set(['ClassDeclaration', 'ClassExpression', 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression', 'ObjectMethod', 'ClassMethod', 'ClassPrivateMethod', 'BlockStatement', 'IfStatement', 'ObjectExpression', 'ArrayExpression', 'ObjectPattern', 'ParenthesizedExpression', 'JSXElement', 'JSXFragment', 'TSEnumDeclaration']);
|
|
8075
|
+
/**
|
|
8076
|
+
* Counts the renderer indentation levels that nest `node`, so comments can be
|
|
8077
|
+
* placed at the column the stringifier will use (`2` spaces per level).
|
|
8078
|
+
* @param {Node} node - The annotated node.
|
|
8079
|
+
* @param {Node[]} parents - The parent stack.
|
|
8080
|
+
* @returns {number} The indentation depth of the node.
|
|
8081
|
+
*/
|
|
8082
|
+
function renderIndentDepth(node, parents) {
|
|
8083
|
+
const index = parents.findLastIndex(_ => _ === node);
|
|
8084
|
+
let depth = 0;
|
|
8085
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
8086
|
+
if (indentingNodeTypes.has(parents[i].type)) {
|
|
8087
|
+
depth++;
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
return depth;
|
|
8091
|
+
}
|
|
8092
|
+
/**
|
|
8093
|
+
* Attaches `@type` comments for typed variable declarations.
|
|
8094
|
+
* @param {import('@babel/types').VariableDeclaration} node - The declaration node.
|
|
8095
|
+
* @param {Node[]} parents - The parent stack.
|
|
8096
|
+
*/
|
|
8097
|
+
function collectVariableTypeComments(node, parents) {
|
|
8098
|
+
for (const declarator of node.declarations) {
|
|
8099
|
+
var _declarator$id;
|
|
8100
|
+
const type = (_declarator$id = declarator.id) == null || (_declarator$id = _declarator$id.typeAnnotation) == null ? void 0 : _declarator$id.typeAnnotation;
|
|
8101
|
+
if (!type) {
|
|
8102
|
+
continue;
|
|
8103
|
+
}
|
|
8104
|
+
attachComment(node, [`@type {${tsTypeToJSDoc(type)}}`], parents);
|
|
8105
|
+
break; // only annotate once per declaration statement
|
|
8106
|
+
}
|
|
8107
|
+
}
|
|
8108
|
+
/**
|
|
8109
|
+
* A roundtrip between TypeScript code -> JavaScript code with JSDoc types.
|
|
8110
|
+
* @param {string} code - The TypeScript code.
|
|
8111
|
+
* @param {object} [options] - Options for the conversion.
|
|
8112
|
+
* @param {boolean} [options.filename] - Unused placeholder, kept for API symmetry.
|
|
8113
|
+
* @returns {string} The converted JavaScript code.
|
|
8114
|
+
*/
|
|
8115
|
+
function ts2js(code, options = {}) {
|
|
8116
|
+
const ast = parseTS(code);
|
|
8117
|
+
annotate(ast.program, []);
|
|
8118
|
+
const stringifier = new ToJS();
|
|
8119
|
+
const source = stringifier.toSource(ast);
|
|
8120
|
+
return formatCommentBreaks(stringifier.getHeader() + source);
|
|
8121
|
+
}
|
|
8122
|
+
/**
|
|
8123
|
+
* Moves a statement that follows a generated JSDoc block onto its own line,
|
|
8124
|
+
* e.g. a closing comment marker directly followed by `export function f() {}`
|
|
8125
|
+
* becomes the closing marker, a newline, then `export function f() {}`.
|
|
8126
|
+
* @param {string} source - The generated source code.
|
|
8127
|
+
* @returns {string} The source with fixed comment/statement breaks.
|
|
8128
|
+
*/
|
|
8129
|
+
function formatCommentBreaks(source) {
|
|
8130
|
+
return source.replace(/ \*\/ +(?=[a-zA-Z_$])/g, '*/\n');
|
|
8131
|
+
}
|
|
8132
|
+
/**
|
|
8133
|
+
* Parses TypeScript code, automatically falling back to TSX (JSX) mode
|
|
8134
|
+
* when the input contains JSX elements.
|
|
8135
|
+
* @param {string} code - The TypeScript code.
|
|
8136
|
+
* @returns {import('@babel/parser').ParseResult<import('@babel/types').File>} The parsed AST.
|
|
8137
|
+
*/
|
|
8138
|
+
function parseTS(code) {
|
|
8139
|
+
try {
|
|
8140
|
+
return parse(code, {
|
|
8141
|
+
sourceType: 'module',
|
|
8142
|
+
plugins: ['typescript']
|
|
8143
|
+
});
|
|
8144
|
+
} catch (e) {
|
|
8145
|
+
return parse(code, {
|
|
8146
|
+
sourceType: 'module',
|
|
8147
|
+
plugins: [['typescript', {
|
|
8148
|
+
isTSX: true
|
|
8149
|
+
}], 'jsx']
|
|
8150
|
+
});
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
|
|
8154
|
+
/**
|
|
8155
|
+
* @file
|
|
8156
|
+
* @todo Consider rendering nested namespaces leading flat, so their member
|
|
8157
|
+
* columns align with the source indentation.
|
|
8158
|
+
*/
|
|
8159
|
+
|
|
8160
|
+
export { Asserter, JSDocAnnotator, Stringifier, ToJS, WATConverter, addTypeChecks, annotateOptional, ast2json, ast2jsonForComparison, capitalize, code2ast2code, compareAST, expandType, expandTypeBabelTS, expandTypeDepFree, extractCurlyContent, extractNameAndOptionality, nodeChildren, nodeIsFunctionLike, parseJSDoc, parseJSDocSetter, parseJSDocTemplates, parseJSDocTypedef, parseType, parseTypeBabelTS, parserOptions, requiredTypeofs, simplifyType, simplifyTypeToSource, toSourceBabelTS, toSourceTS, trimEndSpaces, ts2js, tsTypeToJSDoc };
|