typenative 0.0.21 → 0.1.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/.github/workflows/npm-publish-github-packages.yml +1 -1
- package/CHANGELOG.md +38 -0
- package/README.md +41 -4
- package/TODO.md +9 -5
- package/bin/index.js +1 -2
- package/bin/parse-node.js +2 -2
- package/bin/transpiler.js +3213 -301
- package/package.json +54 -33
- package/tsparser/main.go +2 -2
- package/tsparser/tsparser.exe +2 -2
package/bin/transpiler.js
CHANGED
|
@@ -9,7 +9,7 @@ function isArrayLiteralExpression(n) {
|
|
|
9
9
|
return n?.kind === 'ArrayLiteralExpression';
|
|
10
10
|
}
|
|
11
11
|
function isArrayTypeNode(n) {
|
|
12
|
-
return n?.kind === '
|
|
12
|
+
return n?.kind === 'ArrayType';
|
|
13
13
|
}
|
|
14
14
|
function isArrowFunction(n) {
|
|
15
15
|
return n?.kind === 'ArrowFunction';
|
|
@@ -45,7 +45,7 @@ function isConditionalExpression(n) {
|
|
|
45
45
|
return n?.kind === 'ConditionalExpression';
|
|
46
46
|
}
|
|
47
47
|
function isConstructorDeclaration(n) {
|
|
48
|
-
return n?.kind === '
|
|
48
|
+
return n?.kind === 'Constructor';
|
|
49
49
|
}
|
|
50
50
|
function isDefaultClause(n) {
|
|
51
51
|
return n?.kind === 'DefaultClause';
|
|
@@ -84,7 +84,7 @@ function isFunctionExpression(n) {
|
|
|
84
84
|
return n?.kind === 'FunctionExpression';
|
|
85
85
|
}
|
|
86
86
|
function isFunctionTypeNode(n) {
|
|
87
|
-
return n?.kind === '
|
|
87
|
+
return n?.kind === 'FunctionType';
|
|
88
88
|
}
|
|
89
89
|
function isGetAccessor(n) {
|
|
90
90
|
return n?.kind === 'GetAccessor';
|
|
@@ -102,7 +102,7 @@ function isInterfaceDeclaration(n) {
|
|
|
102
102
|
return n?.kind === 'InterfaceDeclaration';
|
|
103
103
|
}
|
|
104
104
|
function isLiteralTypeNode(n) {
|
|
105
|
-
return n?.kind === '
|
|
105
|
+
return n?.kind === 'LiteralType';
|
|
106
106
|
}
|
|
107
107
|
function isMethodDeclaration(n) {
|
|
108
108
|
return n?.kind === 'MethodDeclaration';
|
|
@@ -198,10 +198,10 @@ function isTypeAssertionExpression(n) {
|
|
|
198
198
|
return n?.kind === 'TypeAssertionExpression';
|
|
199
199
|
}
|
|
200
200
|
function isTypeReferenceNode(n) {
|
|
201
|
-
return n?.kind === '
|
|
201
|
+
return n?.kind === 'TypeReference';
|
|
202
202
|
}
|
|
203
203
|
function isUnionTypeNode(n) {
|
|
204
|
-
return n?.kind === '
|
|
204
|
+
return n?.kind === 'UnionType';
|
|
205
205
|
}
|
|
206
206
|
function isVariableDeclaration(n) {
|
|
207
207
|
return n?.kind === 'VariableDeclaration';
|
|
@@ -250,6 +250,15 @@ function goSafeId() {
|
|
|
250
250
|
return id;
|
|
251
251
|
}
|
|
252
252
|
let parseFunction;
|
|
253
|
+
// Declarations of every parsed file, collected before visiting (functions are
|
|
254
|
+
// hoisted, so call sites may precede them) for contextual typing
|
|
255
|
+
const declaredFunctions = new Map();
|
|
256
|
+
const declaredInterfaces = new Map();
|
|
257
|
+
const declaredTypeAliases = new Map();
|
|
258
|
+
// Package-level declarations of the main file's top-level variables
|
|
259
|
+
const mainPackageVariables = [];
|
|
260
|
+
// True while emitting a separate Go file, whose top-level statements sit at package scope
|
|
261
|
+
let emittingModuleFile = false;
|
|
253
262
|
const importedPackages = new Set();
|
|
254
263
|
let outsideNodes = [];
|
|
255
264
|
const classNames = new Set();
|
|
@@ -265,6 +274,21 @@ const dangerousNames = new Set([
|
|
|
265
274
|
'select', 'struct', 'switch', 'type', 'var',
|
|
266
275
|
]);
|
|
267
276
|
const renamedFunctions = new Map();
|
|
277
|
+
// Go type of the receiver of the method call being emitted (for method handlers)
|
|
278
|
+
let currentReceiverGoType;
|
|
279
|
+
// Struct field / member names: Go keywords get a trailing underscore
|
|
280
|
+
function goFieldName(name) {
|
|
281
|
+
return name !== 'main' && dangerousNames.has(name) ? `${name}_` : name;
|
|
282
|
+
}
|
|
283
|
+
function isFieldNameIdentifier(node) {
|
|
284
|
+
const parent = node.parent;
|
|
285
|
+
if (!parent || parent.name !== node)
|
|
286
|
+
return false;
|
|
287
|
+
return (isPropertyAccessExpression(parent) ||
|
|
288
|
+
isPropertyAssignment(parent) ||
|
|
289
|
+
isPropertySignature(parent) ||
|
|
290
|
+
isPropertyDeclaration(parent));
|
|
291
|
+
}
|
|
268
292
|
const variableTypes = new Map();
|
|
269
293
|
const variableGoTypes = new Map();
|
|
270
294
|
const variableClassNames = new Map();
|
|
@@ -329,7 +353,44 @@ function operatorTokenText(token) {
|
|
|
329
353
|
function defaultParseFunction(_code) {
|
|
330
354
|
throw new Error('transpileToNative: no parse function injected (options.parse)');
|
|
331
355
|
}
|
|
356
|
+
// Parses via the injected parser and links each node to its parent: the JSON
|
|
357
|
+
// AST carries no parent pointers, but the transpiler walks up via node.parent.
|
|
358
|
+
function parseSource(code) {
|
|
359
|
+
const sourceFile = parseFunction(code);
|
|
360
|
+
linkParents(sourceFile, undefined);
|
|
361
|
+
for (const stmt of sourceFile.statements ?? []) {
|
|
362
|
+
if (!stmt.name || !isIdentifier(stmt.name))
|
|
363
|
+
continue;
|
|
364
|
+
if (isFunctionDeclaration(stmt))
|
|
365
|
+
declaredFunctions.set(stmt.name.text, stmt);
|
|
366
|
+
else if (isInterfaceDeclaration(stmt))
|
|
367
|
+
declaredInterfaces.set(stmt.name.text, stmt);
|
|
368
|
+
else if (isTypeAliasDeclaration(stmt))
|
|
369
|
+
declaredTypeAliases.set(stmt.name.text, stmt.type);
|
|
370
|
+
}
|
|
371
|
+
return sourceFile;
|
|
372
|
+
}
|
|
373
|
+
function linkParents(node, parent) {
|
|
374
|
+
node.parent = parent;
|
|
375
|
+
for (const key of Object.keys(node)) {
|
|
376
|
+
if (key === 'parent')
|
|
377
|
+
continue;
|
|
378
|
+
const value = node[key];
|
|
379
|
+
if (Array.isArray(value)) {
|
|
380
|
+
for (const child of value) {
|
|
381
|
+
if (child && typeof child === 'object' && child.kind)
|
|
382
|
+
linkParents(child, node);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else if (value && typeof value === 'object' && value.kind) {
|
|
386
|
+
linkParents(value, node);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
332
390
|
// Renders a type node's source text syntactically (no typechecker needed).
|
|
391
|
+
function isAnyTypeNode(n) {
|
|
392
|
+
return n?.kind === 'AnyKeyword' || n?.kind === 'UnknownKeyword';
|
|
393
|
+
}
|
|
333
394
|
function typeNodeToText(node) {
|
|
334
395
|
if (!node)
|
|
335
396
|
return 'any';
|
|
@@ -349,13 +410,13 @@ function typeNodeToText(node) {
|
|
|
349
410
|
};
|
|
350
411
|
if (keywordMap[node.kind])
|
|
351
412
|
return keywordMap[node.kind];
|
|
352
|
-
if (node.kind === '
|
|
413
|
+
if (node.kind === 'TypeReference' && node.typeName) {
|
|
353
414
|
return typeNodeToText(node.typeName);
|
|
354
415
|
}
|
|
355
416
|
if (node.kind === 'Identifier')
|
|
356
417
|
return node.text;
|
|
357
|
-
if (node.kind === '
|
|
358
|
-
return `${
|
|
418
|
+
if (node.kind === 'ArrayType' && node.elementType) {
|
|
419
|
+
return `${typeNodeToText(node.elementType)}[]`;
|
|
359
420
|
}
|
|
360
421
|
return 'any';
|
|
361
422
|
}
|
|
@@ -363,7 +424,7 @@ function typeNodeToText(node) {
|
|
|
363
424
|
function childNodes(node) {
|
|
364
425
|
const out = [];
|
|
365
426
|
for (const key of Object.keys(node ?? {})) {
|
|
366
|
-
if (key === 'kind' || key === 'text')
|
|
427
|
+
if (key === 'kind' || key === 'text' || key === 'parent')
|
|
367
428
|
continue;
|
|
368
429
|
const v = node[key];
|
|
369
430
|
if (v && typeof v === 'object' && typeof v.kind === 'string') {
|
|
@@ -391,8 +452,14 @@ const helperProvidedPackages = new Set();
|
|
|
391
452
|
export function transpileToNative(code, options) {
|
|
392
453
|
fileResolver = options?.readFile ?? null;
|
|
393
454
|
parseFunction = options?.parse ?? defaultParseFunction;
|
|
455
|
+
declaredFunctions.clear();
|
|
456
|
+
declaredInterfaces.clear();
|
|
457
|
+
declaredTypeAliases.clear();
|
|
458
|
+
variableTypeNodes.clear();
|
|
459
|
+
narrowedVariables.clear();
|
|
460
|
+
nodeCallResultTypes.clear();
|
|
394
461
|
currentFileDir = null;
|
|
395
|
-
const sourceFile =
|
|
462
|
+
const sourceFile = parseSource(code);
|
|
396
463
|
importedPackages.clear();
|
|
397
464
|
outsideNodes = [];
|
|
398
465
|
classNames.clear();
|
|
@@ -415,11 +482,14 @@ export function transpileToNative(code, options) {
|
|
|
415
482
|
classStaticProps.clear();
|
|
416
483
|
usedHelpers.clear();
|
|
417
484
|
helperProvidedPackages.clear();
|
|
485
|
+
mainPackageVariables.length = 0;
|
|
418
486
|
const transpiledCode = visit(sourceFile, { addFunctionOutside: true });
|
|
419
487
|
const transpiledCodeOutside = outsideNodes.map((n) => visit(n, { isOutside: true })).join('\n');
|
|
420
488
|
const main = `package main
|
|
421
489
|
|
|
422
|
-
${[...importedPackages].map((pkg) => `import "${pkg}"`).join('\n')}
|
|
490
|
+
${[...importedPackages].sort().map((pkg) => `import "${pkg}"`).join('\n')}
|
|
491
|
+
|
|
492
|
+
${mainPackageVariables.join('\n')}
|
|
423
493
|
|
|
424
494
|
func main() {
|
|
425
495
|
${transpiledCode.trim()}
|
|
@@ -431,6 +501,13 @@ ${emitGoHelpers()}`.trimEnd();
|
|
|
431
501
|
}
|
|
432
502
|
export function visit(node, options = {}) {
|
|
433
503
|
let code = '';
|
|
504
|
+
// Reads of a narrowed nullable property path dereference it
|
|
505
|
+
if (narrowedVariables.size > 0 && isPropertyAccessExpression(node) && !options.skipNarrowing) {
|
|
506
|
+
const key = getNarrowingKey(node);
|
|
507
|
+
if (key && narrowedVariables.has(key) && isNarrowableReference(node)) {
|
|
508
|
+
return `(*${visit(node, { ...options, skipNarrowing: true })})`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
434
511
|
if (isSourceFile(node)) {
|
|
435
512
|
return (node.statements ?? [])
|
|
436
513
|
.map((n) => visit(n, { addFunctionOutside: true }))
|
|
@@ -438,18 +515,34 @@ export function visit(node, options = {}) {
|
|
|
438
515
|
.join(options.inline ? '' : '\n\t');
|
|
439
516
|
}
|
|
440
517
|
else if (isIdentifier(node)) {
|
|
518
|
+
if (isFieldNameIdentifier(node))
|
|
519
|
+
return goFieldName(node.text);
|
|
520
|
+
if (node.text === 'Boolean' && isCallExpression(node.parent) && node.parent.arguments?.[0] === node) {
|
|
521
|
+
const receiver = isPropertyAccessExpression(node.parent.expression)
|
|
522
|
+
? inferExpressionType(node.parent.expression.expression)
|
|
523
|
+
: undefined;
|
|
524
|
+
const elementType = receiver?.startsWith('[]') ? receiver.slice(2) : 'interface{}';
|
|
525
|
+
return `func(__v ${elementType}) bool { return ${truthinessCheck('__v', elementType)} }`;
|
|
526
|
+
}
|
|
441
527
|
if (node.text === 'undefined')
|
|
442
528
|
return 'nil';
|
|
529
|
+
if (node.text === 'NaN' || node.text === 'Infinity') {
|
|
530
|
+
importedPackages.add('math');
|
|
531
|
+
return node.text === 'NaN' ? 'math.NaN()' : 'math.Inf(1)';
|
|
532
|
+
}
|
|
443
533
|
const goAlias = importAliases.get(node.text);
|
|
444
534
|
if (goAlias)
|
|
445
535
|
return goAlias;
|
|
536
|
+
if (narrowedVariables.has(node.text) && isNarrowableReference(node)) {
|
|
537
|
+
return `(*${getSafeName(node.text)})`;
|
|
538
|
+
}
|
|
446
539
|
return getSafeName(node.text);
|
|
447
540
|
}
|
|
448
541
|
else if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
|
|
449
542
|
return toGoStringLiteral(node.text);
|
|
450
543
|
}
|
|
451
544
|
else if (isAsExpression(node)) {
|
|
452
|
-
return
|
|
545
|
+
return toGoValueOfType(node.expression, getAsExpressionType(node));
|
|
453
546
|
}
|
|
454
547
|
else if (isTypeAssertionExpression(node)) {
|
|
455
548
|
return visit(node.expression);
|
|
@@ -480,7 +573,7 @@ export function visit(node, options = {}) {
|
|
|
480
573
|
return `regexp.MustCompile("${goFlags}${escaped}")`;
|
|
481
574
|
}
|
|
482
575
|
else if (isArrayLiteralExpression(node)) {
|
|
483
|
-
const type =
|
|
576
|
+
const type = getArrayLiteralElementType(node);
|
|
484
577
|
const hasSpread = (node.elements ?? []).some((e) => isSpreadElement(e));
|
|
485
578
|
if (hasSpread) {
|
|
486
579
|
return visitSpreadArrayLiteral(node, type);
|
|
@@ -488,17 +581,50 @@ export function visit(node, options = {}) {
|
|
|
488
581
|
return `[]${type} {${(node.elements ?? []).map((e) => visit(e)).join(', ')}}`;
|
|
489
582
|
}
|
|
490
583
|
else if (isBlock(node)) {
|
|
491
|
-
return `{\n\t\t${options.prefixBlockContent ?? ''}${(node.statements ?? [])
|
|
492
|
-
.map((n) => visit(n))
|
|
493
|
-
.join('\t')}${options.extraBlockContent ?? ''}}${options.inline ? '' : '\n\t'}`;
|
|
584
|
+
return `{\n\t\t${options.prefixBlockContent ?? ''}${visitBlockStatements(node.statements ?? [])}${options.extraBlockContent ?? ''}}${options.inline ? '' : '\n\t'}`;
|
|
494
585
|
}
|
|
495
586
|
else if (isElementAccessExpression(node)) {
|
|
496
587
|
if (hasQuestionDot(node)) {
|
|
497
588
|
return visitOptionalElementAccess(node);
|
|
498
589
|
}
|
|
590
|
+
if (isDynamicValue(node.expression)) {
|
|
591
|
+
useHelper('dynamic');
|
|
592
|
+
return `TnIndex(${visit(node.expression)}, ${visit(node.argumentExpression)})`;
|
|
593
|
+
}
|
|
594
|
+
// process.env['X'] → TnGetenv("X")
|
|
595
|
+
if (isProcessEnv(node.expression)) {
|
|
596
|
+
useHelper('getenv');
|
|
597
|
+
return `TnGetenv(${visit(node.argumentExpression)})`;
|
|
598
|
+
}
|
|
599
|
+
// Maps (Record/Map) and string keys index directly; arrays/strings need an int index
|
|
600
|
+
const targetType = inferExpressionType(node.expression);
|
|
601
|
+
if (targetType === 'string') {
|
|
602
|
+
// s[i] in JS is a one-character string ("" when out of range)
|
|
603
|
+
useHelper('dynamic');
|
|
604
|
+
return `TnCharAt(${visit(node.expression)}, ${visit(node.argumentExpression)})`;
|
|
605
|
+
}
|
|
606
|
+
if (targetType?.startsWith('*TnMap[')) {
|
|
607
|
+
return `${visit(node.expression)}.Get(${toGoValueOfType(node.argumentExpression, extractMapKeyType(targetType))})`;
|
|
608
|
+
}
|
|
609
|
+
if (isStringLiteral(node.argumentExpression)) {
|
|
610
|
+
return `${visit(node.expression)}[${visit(node.argumentExpression)}]`;
|
|
611
|
+
}
|
|
612
|
+
// Reading past the end of an array gives undefined in JS: the zero value here
|
|
613
|
+
if (targetType?.startsWith('[]') && !isAssignmentTarget(node)) {
|
|
614
|
+
useHelper('dynamic');
|
|
615
|
+
return `TnAt(${visit(node.expression)}, ${visit(node.argumentExpression)})`;
|
|
616
|
+
}
|
|
499
617
|
return `${visit(node.expression)}[int(${visit(node.argumentExpression)})]`;
|
|
500
618
|
}
|
|
501
619
|
else if (isPropertyAccessExpression(node)) {
|
|
620
|
+
if (isDynamicValue(node.expression) && node.name.text === 'length' && !isCallee(node)) {
|
|
621
|
+
useHelper('dynamic');
|
|
622
|
+
return `TnLength(${visit(node.expression)})`;
|
|
623
|
+
}
|
|
624
|
+
if (isDynamicValue(node.expression) && !isCallee(node) && !isProcessEnv(node.expression)) {
|
|
625
|
+
useHelper('dynamic');
|
|
626
|
+
return `TnGet(${visit(node.expression)}, "${node.name.text}")`;
|
|
627
|
+
}
|
|
502
628
|
if (hasQuestionDot(node)) {
|
|
503
629
|
return visitOptionalPropertyAccess(node);
|
|
504
630
|
}
|
|
@@ -538,7 +664,9 @@ export function visit(node, options = {}) {
|
|
|
538
664
|
}
|
|
539
665
|
// Array destructuring: const [a, b] = arr
|
|
540
666
|
if (isArrayBindingPattern(node.name) && node.initializer) {
|
|
541
|
-
const initExpr =
|
|
667
|
+
const initExpr = isDynamicValue(node.initializer)
|
|
668
|
+
? toGoValueOfType(node.initializer, '[]interface{}')
|
|
669
|
+
: visit(node.initializer);
|
|
542
670
|
const tmpVar = getTempName('arr');
|
|
543
671
|
const parts = [`${tmpVar} := ${initExpr}`];
|
|
544
672
|
node.name.elements.forEach((el, idx) => {
|
|
@@ -556,12 +684,31 @@ export function visit(node, options = {}) {
|
|
|
556
684
|
});
|
|
557
685
|
return parts.join(';\n\t');
|
|
558
686
|
}
|
|
559
|
-
const
|
|
687
|
+
// `const x: any = expr` keeps inference (x := expr); `any` elsewhere is interface{}
|
|
688
|
+
const isInferredAny = isAnyTypeNode(node.type) && !!node.initializer;
|
|
689
|
+
const type = isInferredAny ? ':' : getType(node.type);
|
|
560
690
|
// Track variable type for type-aware method dispatch
|
|
561
691
|
if (isIdentifier(node.name)) {
|
|
562
|
-
|
|
692
|
+
// A declaration replaces whatever an earlier variable of the same name recorded
|
|
693
|
+
const inferredType = node.initializer && (!node.type || isInferredAny)
|
|
694
|
+
? inferExpressionType(node.initializer)
|
|
695
|
+
: undefined;
|
|
696
|
+
variableTypes.delete(node.name.text);
|
|
697
|
+
variableClassNames.delete(node.name.text);
|
|
698
|
+
narrowedVariables.delete(node.name.text);
|
|
699
|
+
if (node.type && !isInferredAny)
|
|
700
|
+
variableTypeNodes.set(node.name.text, node.type);
|
|
701
|
+
else
|
|
702
|
+
variableTypeNodes.delete(node.name.text);
|
|
703
|
+
if (node.type && !isInferredAny) {
|
|
563
704
|
variableGoTypes.set(node.name.text, getType(node.type));
|
|
564
705
|
}
|
|
706
|
+
else if (inferredType) {
|
|
707
|
+
variableGoTypes.set(node.name.text, inferredType);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
variableGoTypes.delete(node.name.text);
|
|
711
|
+
}
|
|
565
712
|
const cat = node.type ? getTypeCategory(node.type) : undefined;
|
|
566
713
|
if (cat) {
|
|
567
714
|
variableTypes.set(node.name.text, cat);
|
|
@@ -586,22 +733,25 @@ export function visit(node, options = {}) {
|
|
|
586
733
|
else if (node.initializer && isRegularExpressionLiteral(node.initializer)) {
|
|
587
734
|
variableTypes.set(node.name.text, 'RegExp');
|
|
588
735
|
}
|
|
589
|
-
if (!variableGoTypes.has(node.name.text) && node.initializer) {
|
|
590
|
-
const inferredType = inferExpressionType(node.initializer);
|
|
591
|
-
if (inferredType) {
|
|
592
|
-
variableGoTypes.set(node.name.text, inferredType);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
736
|
}
|
|
596
737
|
let initializer = node.initializer ? `= ${visit(node.initializer)}` : '';
|
|
597
738
|
// Wrap non-nil values assigned to nullable primitive pointer types
|
|
598
739
|
// Only wrap for primitive pointers (*string, *float64, *bool), not class pointers
|
|
599
|
-
if (node.initializer &&
|
|
600
|
-
type
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
740
|
+
if (node.initializer && (type.startsWith('*') || isDynamicValue(node.initializer))) {
|
|
741
|
+
initializer = `= ${toGoValueOfType(node.initializer, type)}`;
|
|
742
|
+
}
|
|
743
|
+
// Package scope has no `:=`: module-level declarations need `var x = expr`
|
|
744
|
+
const isTopLevelStatement = isVariableStatement(node.parent?.parent) && isSourceFile(node.parent?.parent?.parent);
|
|
745
|
+
const isPackageScope = emittingModuleFile && isTopLevelStatement;
|
|
746
|
+
if (type === ':' && isPackageScope) {
|
|
747
|
+
return `var ${visit(node.name)} ${initializer}`;
|
|
748
|
+
}
|
|
749
|
+
const isMainTopLevel = !emittingModuleFile && isTopLevelStatement;
|
|
750
|
+
const packageType = type === ':' ? variableGoTypes.get(node.name.text) : type;
|
|
751
|
+
if (isMainTopLevel && packageType && packageType !== 'nil' && isIdentifier(node.name)) {
|
|
752
|
+
const name = visit(node.name);
|
|
753
|
+
mainPackageVariables.push(`var ${name} ${packageType}`);
|
|
754
|
+
return initializer ? `${name} ${initializer}` : '';
|
|
605
755
|
}
|
|
606
756
|
return `${type === ':' ? '' : 'var '}${visit(node.name)} ${type}${type === ':' ? '' : ' '}${initializer}`;
|
|
607
757
|
}
|
|
@@ -610,6 +760,18 @@ export function visit(node, options = {}) {
|
|
|
610
760
|
(isPropertyAccessExpression(node.expression) && hasQuestionDot(node.expression))) {
|
|
611
761
|
return visitOptionalCall(node);
|
|
612
762
|
}
|
|
763
|
+
if (isPropertyAccessExpression(node.expression) && isOptionalChain(node.expression.expression)) {
|
|
764
|
+
const receiverType = inferExpressionType(node.expression.expression);
|
|
765
|
+
if (receiverType && NULLABLE_PRIMITIVE_TYPES.includes(receiverType)) {
|
|
766
|
+
return visitNullablePrimitiveOptionalCall(node, visit(node.expression.expression), receiverType);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const dynamicCall = visitDynamicMethodCall(node);
|
|
770
|
+
if (dynamicCall)
|
|
771
|
+
return dynamicCall;
|
|
772
|
+
const regexReplace = visitRegexReplace(node);
|
|
773
|
+
if (regexReplace)
|
|
774
|
+
return regexReplace;
|
|
613
775
|
// IIFE with named function expression: (function name() { ... })()
|
|
614
776
|
if (isParenthesizedExpression(node.expression) &&
|
|
615
777
|
isFunctionExpression(node.expression.expression)) {
|
|
@@ -621,7 +783,7 @@ export function visit(node, options = {}) {
|
|
|
621
783
|
const inferredRetType = inferFunctionBodyReturnType(fn);
|
|
622
784
|
const returnType = inferredRetType ? ` ${inferredRetType}` : '';
|
|
623
785
|
const args = (node.arguments ?? []).map((a) => visit(a)).join(', ');
|
|
624
|
-
return `func(${parameterInfo.signature})${returnType} ${visit(fn.body, { prefixBlockContent: parameterInfo.prefixBlockContent })}(${args})`;
|
|
786
|
+
return `func(${parameterInfo.signature})${returnType} ${visit(fn.body, { prefixBlockContent: parameterInfo.prefixBlockContent }).trimEnd()}(${args})`;
|
|
625
787
|
}
|
|
626
788
|
// Handle setTimeout specially to get raw delay value
|
|
627
789
|
if (isIdentifier(node.expression) && node.expression.text === 'setTimeout') {
|
|
@@ -640,21 +802,37 @@ export function visit(node, options = {}) {
|
|
|
640
802
|
const typeArgs = getTypeArguments((node.typeArguments ?? []));
|
|
641
803
|
// Handle spread arguments: fn(...arr) → fn(arr...)
|
|
642
804
|
const hasSpreadArg = (node.arguments ?? []).some((a) => isSpreadElement(a));
|
|
643
|
-
const args = hasSpreadArg
|
|
644
|
-
? (node.arguments ?? []).map((a) => isSpreadElement(a) ? `${visit(a.expression)}...` : visit(a))
|
|
645
|
-
: (node.arguments ?? []).map((a) => visit(a));
|
|
805
|
+
const args = hasSpreadArg ? visitSpreadCallArguments(node) : visitCallArguments(node);
|
|
646
806
|
// Resolve object type for type-aware method dispatch
|
|
647
807
|
let objectType;
|
|
648
808
|
if (isPropertyAccessExpression(node.expression)) {
|
|
649
809
|
objectType = resolveExpressionType(node.expression.expression);
|
|
650
810
|
}
|
|
811
|
+
currentReceiverGoType = isPropertyAccessExpression(node.expression)
|
|
812
|
+
? inferExpressionType(node.expression.expression)
|
|
813
|
+
: undefined;
|
|
651
814
|
return getCallString(safeCaller, args, typeArgs, objectType);
|
|
652
815
|
}
|
|
653
816
|
else if (isPrefixUnaryExpression(node)) {
|
|
817
|
+
if (node.operator === 'ExclamationToken') {
|
|
818
|
+
return `!${wrapCondition(toGoCondition(node.operand))}`;
|
|
819
|
+
}
|
|
820
|
+
if (isIncrementOrDecrement(node))
|
|
821
|
+
return visitIncrementOrDecrement(node, true);
|
|
822
|
+
if (node.operator === 'TildeToken') {
|
|
823
|
+
useHelper('dynamic');
|
|
824
|
+
return `float64(^TnInt32(${visit(node.operand)}))`;
|
|
825
|
+
}
|
|
654
826
|
return `${getOperatorText(node.operator)}${visit(node.operand)}`;
|
|
655
827
|
}
|
|
656
828
|
else if (isPostfixUnaryExpression(node)) {
|
|
657
|
-
return
|
|
829
|
+
return visitIncrementOrDecrement(node, false);
|
|
830
|
+
}
|
|
831
|
+
else if (node.kind === 'DeleteExpression') {
|
|
832
|
+
return visitDelete(node);
|
|
833
|
+
}
|
|
834
|
+
else if (node.kind === 'TypeOfExpression') {
|
|
835
|
+
return visitTypeOf(node);
|
|
658
836
|
}
|
|
659
837
|
else if (isConditionalExpression(node)) {
|
|
660
838
|
return visitConditionalExpression(node);
|
|
@@ -663,12 +841,57 @@ export function visit(node, options = {}) {
|
|
|
663
841
|
if (node.operatorToken.kind === 'QuestionQuestionToken') {
|
|
664
842
|
return visitNullishCoalescingExpression(node);
|
|
665
843
|
}
|
|
844
|
+
if (node.operatorToken.kind === 'InstanceOfKeyword' && isIdentifier(node.right)) {
|
|
845
|
+
// x instanceof C: the runtime type is C's pointer type
|
|
846
|
+
return `func() bool { _, ok := interface{}(${visit(node.left)}).(*${getSafeName(node.right.text)}); return ok }()`;
|
|
847
|
+
}
|
|
848
|
+
if (node.operatorToken.kind === 'InKeyword') {
|
|
849
|
+
if (isDynamicValue(node.right)) {
|
|
850
|
+
useHelper('dynamic');
|
|
851
|
+
return `TnHas(${visit(node.right)}, ${visit(node.left)})`;
|
|
852
|
+
}
|
|
853
|
+
return `${visit(node.right)}.Has(${visit(node.left)})`;
|
|
854
|
+
}
|
|
855
|
+
if (isLogicalOperator(node.operatorToken)) {
|
|
856
|
+
const logical = visitLogicalExpression(node);
|
|
857
|
+
if (logical)
|
|
858
|
+
return logical;
|
|
859
|
+
}
|
|
860
|
+
// x ??= v → assign only when x is nil
|
|
861
|
+
if (node.operatorToken.kind === 'BarBarEqualsToken' || node.operatorToken.kind === 'AmpersandAmpersandEqualsToken') {
|
|
862
|
+
// a ||= b assigns when a is falsy; a &&= b when a is truthy
|
|
863
|
+
const target = visit(node.left);
|
|
864
|
+
const condition = toGoCondition(node.left);
|
|
865
|
+
const value = toGoValueOfType(node.right, inferExpressionType(node.left));
|
|
866
|
+
const test = node.operatorToken.kind === 'BarBarEqualsToken' ? `!${wrapCondition(condition)}` : condition;
|
|
867
|
+
return `if ${test} { ${target} = ${value} }`;
|
|
868
|
+
}
|
|
869
|
+
if (node.operatorToken.kind === 'QuestionQuestionEqualsToken') {
|
|
870
|
+
const target = visit(node.left);
|
|
871
|
+
const value = toGoValueOfType(node.right, inferExpressionType(node.left));
|
|
872
|
+
return `if ${target} == nil { ${target} = ${value} }`;
|
|
873
|
+
}
|
|
666
874
|
let op = operatorTokenText(node.operatorToken);
|
|
667
875
|
if (op === '===')
|
|
668
876
|
op = '==';
|
|
669
877
|
if (op === '!==')
|
|
670
878
|
op = '!=';
|
|
671
879
|
// Go's % is not defined on float64 (TS numbers all map to float64)
|
|
880
|
+
if (op === '/' && isNumericLiteral(node.right) && Number(node.right.text) === 0) {
|
|
881
|
+
return `func(a, b float64) float64 { return a / b }(${visit(node.left)}, ${visit(node.right)})`;
|
|
882
|
+
}
|
|
883
|
+
const bitwise = visitBitwise(node, op);
|
|
884
|
+
if (bitwise)
|
|
885
|
+
return bitwise;
|
|
886
|
+
if (op === '**') {
|
|
887
|
+
importedPackages.add('math');
|
|
888
|
+
return `math.Pow(${visit(node.left)}, ${visit(node.right)})`;
|
|
889
|
+
}
|
|
890
|
+
if (op === '**=') {
|
|
891
|
+
importedPackages.add('math');
|
|
892
|
+
const target = visit(node.left);
|
|
893
|
+
return `${target} = math.Pow(${target}, ${visit(node.right)})`;
|
|
894
|
+
}
|
|
672
895
|
if (op === '%') {
|
|
673
896
|
importedPackages.add('math');
|
|
674
897
|
return `math.Mod(${visit(node.left)}, ${visit(node.right)})`;
|
|
@@ -678,6 +901,75 @@ export function visit(node, options = {}) {
|
|
|
678
901
|
const left = visit(node.left);
|
|
679
902
|
return `${left} = math.Mod(${left}, ${visit(node.right)})`;
|
|
680
903
|
}
|
|
904
|
+
if (['<', '>', '<=', '>=', '-', '*', '/', '+'].includes(op)) {
|
|
905
|
+
const dynamicArithmetic = visitDynamicArithmetic(node, op);
|
|
906
|
+
if (dynamicArithmetic)
|
|
907
|
+
return dynamicArithmetic;
|
|
908
|
+
}
|
|
909
|
+
if (op === '==' || op === '!=') {
|
|
910
|
+
// any values: JS equality (objects by identity) without Go's uncomparable-type panic
|
|
911
|
+
if ((isDynamicValue(node.left) || isDynamicValue(node.right)) &&
|
|
912
|
+
!isNilLiteral(node.left) &&
|
|
913
|
+
!isNilLiteral(node.right)) {
|
|
914
|
+
useHelper('dynamic');
|
|
915
|
+
const same = `TnSame(${visit(node.left)}, ${visit(node.right)})`;
|
|
916
|
+
return op === '==' ? same : `!${same}`;
|
|
917
|
+
}
|
|
918
|
+
const nullableComparison = visitNullableComparison(node, op);
|
|
919
|
+
if (nullableComparison)
|
|
920
|
+
return nullableComparison;
|
|
921
|
+
// A value type (string, number, struct…) is never null/undefined
|
|
922
|
+
const otherSide = isNilLiteral(node.right) ? node.left : isNilLiteral(node.left) ? node.right : undefined;
|
|
923
|
+
const otherType = otherSide ? inferExpressionType(otherSide) : undefined;
|
|
924
|
+
if (otherType && otherType !== 'nil' && !isNilableGoType(otherType)) {
|
|
925
|
+
return op === '!=' ? 'true' : 'false';
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (op === '=' && isPropertyAccessExpression(node.left) && isDynamicValue(node.left.expression)) {
|
|
929
|
+
useHelper('dynamic');
|
|
930
|
+
return `TnSet(${visit(node.left.expression)}, "${node.left.name.text}", ${visit(node.right)})`;
|
|
931
|
+
}
|
|
932
|
+
// m[k] = v on an ordered map
|
|
933
|
+
if (isElementAccessExpression(node.left) && isAssignmentTarget(node.left)) {
|
|
934
|
+
const mapType = inferExpressionType(node.left.expression);
|
|
935
|
+
if (mapType?.startsWith('*TnMap[')) {
|
|
936
|
+
const map = visit(node.left.expression);
|
|
937
|
+
const key = toGoValueOfType(node.left.argumentExpression, extractMapKeyType(mapType));
|
|
938
|
+
const value = op === '='
|
|
939
|
+
? toGoValueOfType(node.right, extractMapValueType(mapType))
|
|
940
|
+
: `${map}.Get(${key}) ${op.slice(0, -1)} ${visit(node.right)}`;
|
|
941
|
+
return `${map}.Set(${key}, ${value})`;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// arr.length = n truncates the slice
|
|
945
|
+
if (op === '=' &&
|
|
946
|
+
isPropertyAccessExpression(node.left) &&
|
|
947
|
+
node.left.name.text === 'length' &&
|
|
948
|
+
inferExpressionType(node.left.expression)?.startsWith('[]')) {
|
|
949
|
+
const array = visit(node.left.expression);
|
|
950
|
+
return `${array} = ${array}[:int(${visit(node.right)})]`;
|
|
951
|
+
}
|
|
952
|
+
// Assigning to a nullable primitive (*T) boxes the value
|
|
953
|
+
if (op === '=' && isIdentifier(node.left)) {
|
|
954
|
+
const leftType = variableGoTypes.get(node.left.text);
|
|
955
|
+
if (leftType && (leftType.startsWith('*') || isDynamicValue(node.right))) {
|
|
956
|
+
return `${getSafeName(node.left.text)} = ${toGoValueOfType(node.right, leftType)}`;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (op === '&&' || op === '||') {
|
|
960
|
+
const right = withNarrowing(getNarrowedNames(node.left, op === '&&'), () => visit(node.right));
|
|
961
|
+
return `${visit(node.left)} ${op} ${right}`;
|
|
962
|
+
}
|
|
963
|
+
if (op === '+') {
|
|
964
|
+
const leftType = inferExpressionType(node.left);
|
|
965
|
+
const rightType = inferExpressionType(node.right);
|
|
966
|
+
if (leftType === 'string' && rightType !== 'string') {
|
|
967
|
+
return `${visit(node.left)} + ${jsStringOf(visit(node.right), rightType)}`;
|
|
968
|
+
}
|
|
969
|
+
if (rightType === 'string' && leftType !== 'string') {
|
|
970
|
+
return `${jsStringOf(visit(node.left), leftType)} + ${visit(node.right)}`;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
681
973
|
return `${visit(node.left)} ${op} ${visit(node.right)}`;
|
|
682
974
|
}
|
|
683
975
|
else if (isParenthesizedExpression(node)) {
|
|
@@ -694,15 +986,15 @@ export function visit(node, options = {}) {
|
|
|
694
986
|
return visit(node.expression) + (options.inline ? '' : ';\n\t');
|
|
695
987
|
}
|
|
696
988
|
else if (isForStatement(node)) {
|
|
697
|
-
return `for ${visit(node.initializer, { inline: true })}; ${visit(node.
|
|
698
|
-
inline: true
|
|
699
|
-
})}; ${visit(node.incrementor, { inline: true })}${visit(node.statement)}`;
|
|
989
|
+
return `for ${visit(node.initializer, { inline: true })}; ${node.condition ? toGoCondition(node.condition) : ''}; ${visit(node.incrementor, { inline: true })}${visitLoopBody(node.statement)}`;
|
|
700
990
|
}
|
|
701
991
|
else if (isForInStatement(node)) {
|
|
702
992
|
const varName = isVariableDeclarationList(node.initializer)
|
|
703
993
|
? visit(node.initializer.declarations[0].name)
|
|
704
994
|
: visit(node.initializer);
|
|
705
|
-
|
|
995
|
+
const iterated = visit(node.expression, { inline: true });
|
|
996
|
+
const isOrderedMap = inferExpressionType(node.expression)?.startsWith('*TnMap[');
|
|
997
|
+
return `for ${varName} := range ${iterated}${isOrderedMap ? '.All()' : ''}${visitLoopBody(node.statement)}`;
|
|
706
998
|
}
|
|
707
999
|
else if (isForOfStatement(node)) {
|
|
708
1000
|
// Unwrap Object.entries(x) → treat x as the iterable
|
|
@@ -715,31 +1007,38 @@ export function visit(node, options = {}) {
|
|
|
715
1007
|
iterNode.arguments.length > 0) {
|
|
716
1008
|
iterNode = iterNode.arguments[0];
|
|
717
1009
|
}
|
|
718
|
-
const iterExpr =
|
|
1010
|
+
const iterExpr = isDynamicValue(iterNode)
|
|
1011
|
+
? toGoValueOfType(iterNode, '[]interface{}')
|
|
1012
|
+
: visit(iterNode, { inline: true });
|
|
719
1013
|
const iterType = inferExpressionType(iterNode);
|
|
720
|
-
if (iterType && iterType.startsWith('
|
|
1014
|
+
if (iterType && iterType.startsWith('*TnMap[')) {
|
|
721
1015
|
const valueType = extractMapValueType(iterType);
|
|
722
1016
|
const isSet = valueType === 'struct{}';
|
|
723
1017
|
const varInfo = getForOfVarNames(node.initializer);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
return `for ${varInfo[0]}, ${varInfo[1]} := range ${iterExpr}${
|
|
729
|
-
}
|
|
730
|
-
else {
|
|
731
|
-
return `for ${varInfo[0]} := range ${iterExpr}${visit(node.statement)}`;
|
|
1018
|
+
registerLocalVariable(varInfo[0], extractMapKeyType(iterType));
|
|
1019
|
+
if (varInfo.length >= 2 && !isSet)
|
|
1020
|
+
registerLocalVariable(varInfo[1], valueType);
|
|
1021
|
+
if (varInfo.length >= 2 && !isSet) {
|
|
1022
|
+
return `for ${varInfo[0]}, ${varInfo[1]} := range ${iterExpr}.All()${visitLoopBody(node.statement)}`;
|
|
732
1023
|
}
|
|
1024
|
+
return `for ${varInfo[0]} := range ${iterExpr}.All()${visitLoopBody(node.statement)}`;
|
|
733
1025
|
}
|
|
734
|
-
return
|
|
1026
|
+
return visitForOfSequence(node, iterExpr, iterType);
|
|
735
1027
|
}
|
|
736
1028
|
else if (isWhileStatement(node)) {
|
|
737
|
-
|
|
1029
|
+
// while ((x = next()) !== null) → for { x = next(); if !(x != nil) { break }; … }
|
|
1030
|
+
const hoisted = getHoistedConditionAssignment(node.expression);
|
|
1031
|
+
if (hoisted) {
|
|
1032
|
+
const condition = { ...node.expression, left: hoisted.left };
|
|
1033
|
+
const prefix = `${visit(hoisted, { inline: true })};\n\t\tif !(${visit(condition, {
|
|
1034
|
+
inline: true
|
|
1035
|
+
})}) {\n\t\t\tbreak\n\t\t}\n\t\t`;
|
|
1036
|
+
return `for ${visitLoopBody(node.statement, prefix)}`;
|
|
1037
|
+
}
|
|
1038
|
+
return `for ${toGoCondition(node.expression)}${visitLoopBody(node.statement)}`;
|
|
738
1039
|
}
|
|
739
1040
|
else if (isDoStatement(node)) {
|
|
740
|
-
const condition = `\tif !(${
|
|
741
|
-
inline: true
|
|
742
|
-
})}) {\n\t\t\tbreak \n\t\t}\n\t`;
|
|
1041
|
+
const condition = `\tif !(${toGoCondition(node.expression)}) {\n\t\t\tbreak \n\t\t}\n\t`;
|
|
743
1042
|
return `for ${visit(node.statement, { inline: true, extraBlockContent: condition })}`;
|
|
744
1043
|
}
|
|
745
1044
|
else if (isIfStatement(node)) {
|
|
@@ -747,18 +1046,15 @@ export function visit(node, options = {}) {
|
|
|
747
1046
|
// Go requires a block body; `} else` must stay on the same line, so the
|
|
748
1047
|
// terminating ';' is only added when no else branch follows
|
|
749
1048
|
const thenTerm = node.elseStatement ? '' : ';';
|
|
750
|
-
const thenCode = isBlock(node.thenStatement)
|
|
1049
|
+
const thenCode = withNarrowing(getNarrowedNames(node.expression, true), () => isBlock(node.thenStatement)
|
|
751
1050
|
? visit(node.thenStatement, { inline: !!node.elseStatement })
|
|
752
|
-
: `{\n${visit(node.thenStatement)}\n}${thenTerm}
|
|
753
|
-
const condition = `if ${
|
|
1051
|
+
: `{\n${visit(node.thenStatement)}\n}${thenTerm}`);
|
|
1052
|
+
const condition = `if ${toGoCondition(node.expression)} ${thenCode}`;
|
|
754
1053
|
if (node.elseStatement) {
|
|
755
|
-
if (isBlock(node.elseStatement)) {
|
|
756
|
-
return `${condition} else ${visit(node.elseStatement)}`;
|
|
757
|
-
}
|
|
758
1054
|
// else-if chains stay chained; other single statements get a block
|
|
759
|
-
const elseCode = isIfStatement(node.elseStatement)
|
|
1055
|
+
const elseCode = withNarrowing(getNarrowedNames(node.expression, false), () => isBlock(node.elseStatement) || isIfStatement(node.elseStatement)
|
|
760
1056
|
? visit(node.elseStatement)
|
|
761
|
-
: `{\n${visit(node.elseStatement)}\n}
|
|
1057
|
+
: `{\n${visit(node.elseStatement)}\n};\n\t`);
|
|
762
1058
|
return `${condition} else ${elseCode}`;
|
|
763
1059
|
}
|
|
764
1060
|
return condition;
|
|
@@ -785,6 +1081,9 @@ export function visit(node, options = {}) {
|
|
|
785
1081
|
else if (isBreakStatement(node)) {
|
|
786
1082
|
return 'break';
|
|
787
1083
|
}
|
|
1084
|
+
else if (node.kind === 'ContinueStatement') {
|
|
1085
|
+
return 'continue;\n\t';
|
|
1086
|
+
}
|
|
788
1087
|
else if (isThrowStatement(node)) {
|
|
789
1088
|
const expr = node.expression;
|
|
790
1089
|
if (isNewExpression(expr) &&
|
|
@@ -807,7 +1106,14 @@ export function visit(node, options = {}) {
|
|
|
807
1106
|
node.expression.expression.text === 'Promise') {
|
|
808
1107
|
return visitPromiseReturn(node.expression, options);
|
|
809
1108
|
}
|
|
810
|
-
|
|
1109
|
+
const enclosingFn = getEnclosingFunction(node);
|
|
1110
|
+
const isAsync = enclosingFn?.modifiers?.some((m) => m.kind === 'AsyncKeyword');
|
|
1111
|
+
const returnGoType = enclosingFn && !isAsync ? inferFunctionBodyReturnType(enclosingFn) : undefined;
|
|
1112
|
+
const value = node.expression ? toGoValueOfType(node.expression, returnGoType) : '';
|
|
1113
|
+
const terminator = options.inline ? '' : ';\n\t';
|
|
1114
|
+
if (tryReturn && enclosingFn === tryReturn.fn)
|
|
1115
|
+
return tryAwareReturn(value) + terminator;
|
|
1116
|
+
return `return ${value}` + terminator;
|
|
811
1117
|
}
|
|
812
1118
|
else if (isFunctionDeclaration(node) || isFunctionExpression(node)) {
|
|
813
1119
|
if (options.addFunctionOutside) {
|
|
@@ -815,7 +1121,7 @@ export function visit(node, options = {}) {
|
|
|
815
1121
|
return '';
|
|
816
1122
|
}
|
|
817
1123
|
const typeParams = getTypeParameters(node.typeParameters);
|
|
818
|
-
const parameterInfo = getFunctionParametersInfo(
|
|
1124
|
+
const parameterInfo = withContextualParameters(node, getFunctionParametersInfo(node.parameters ?? []));
|
|
819
1125
|
if (node.body && isBlock(node.body)) {
|
|
820
1126
|
prescanVariableDeclarations(node.body);
|
|
821
1127
|
}
|
|
@@ -831,7 +1137,7 @@ export function visit(node, options = {}) {
|
|
|
831
1137
|
if (!node.name) {
|
|
832
1138
|
return `func${typeParams}(${parameterInfo.signature})${returnType} ${visit(node.body, {
|
|
833
1139
|
prefixBlockContent: parameterInfo.prefixBlockContent
|
|
834
|
-
})}`;
|
|
1140
|
+
}).trimEnd()}`;
|
|
835
1141
|
}
|
|
836
1142
|
const name = visit(node.name, { inline: true });
|
|
837
1143
|
const safeName = getSafeName(name);
|
|
@@ -840,7 +1146,7 @@ export function visit(node, options = {}) {
|
|
|
840
1146
|
})}`;
|
|
841
1147
|
}
|
|
842
1148
|
else if (isArrowFunction(node)) {
|
|
843
|
-
const parameterInfo = getFunctionParametersInfo(
|
|
1149
|
+
const parameterInfo = withContextualParameters(node, getFunctionParametersInfo(node.parameters ?? []));
|
|
844
1150
|
const inferredRetType = inferFunctionBodyReturnType(node);
|
|
845
1151
|
const returnType = inferredRetType ? ` ${inferredRetType}` : '';
|
|
846
1152
|
if (parameterInfo.prefixBlockContent && !isBlock(node.body)) {
|
|
@@ -851,7 +1157,7 @@ export function visit(node, options = {}) {
|
|
|
851
1157
|
}
|
|
852
1158
|
return `func(${parameterInfo.signature})${returnType} ${visit(node.body, {
|
|
853
1159
|
prefixBlockContent: parameterInfo.prefixBlockContent
|
|
854
|
-
})}`;
|
|
1160
|
+
}).trimEnd()}`;
|
|
855
1161
|
}
|
|
856
1162
|
else if (node.kind === 'ThisKeyword') {
|
|
857
1163
|
return 'self';
|
|
@@ -868,7 +1174,34 @@ export function visit(node, options = {}) {
|
|
|
868
1174
|
}
|
|
869
1175
|
else if (isTypeAliasDeclaration(node)) {
|
|
870
1176
|
typeAliases.set(node.name.text, node.type);
|
|
871
|
-
|
|
1177
|
+
if (node.type?.kind !== 'TypeLiteral')
|
|
1178
|
+
return '';
|
|
1179
|
+
// type X = { a: T } → type X struct { a T }
|
|
1180
|
+
if (options.addFunctionOutside) {
|
|
1181
|
+
outsideNodes.push(node);
|
|
1182
|
+
const properties = new Map();
|
|
1183
|
+
for (const member of node.type.members ?? []) {
|
|
1184
|
+
if (isPropertySignature(member) && isIdentifier(member.name)) {
|
|
1185
|
+
properties.set(member.name.text, getOptionalNodeType(member.type, !!member.questionToken));
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (properties.size > 0)
|
|
1189
|
+
interfacePropertyTypes.set(visit(node.name), properties);
|
|
1190
|
+
return '';
|
|
1191
|
+
}
|
|
1192
|
+
const localProperties = new Map();
|
|
1193
|
+
for (const member of node.type.members ?? []) {
|
|
1194
|
+
if (isPropertySignature(member) && isIdentifier(member.name)) {
|
|
1195
|
+
localProperties.set(member.name.text, getOptionalNodeType(member.type, !!member.questionToken));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (localProperties.size > 0)
|
|
1199
|
+
interfacePropertyTypes.set(visit(node.name), localProperties);
|
|
1200
|
+
const fields = (node.type.members ?? [])
|
|
1201
|
+
.filter((m) => isPropertySignature(m) && isIdentifier(m.name))
|
|
1202
|
+
.map((m) => `\t${goFieldName(m.name.text)} ${getOptionalNodeType(m.type, !!m.questionToken)}`);
|
|
1203
|
+
const terminator = options.isOutside ? '' : ';\n\t';
|
|
1204
|
+
return `type ${visit(node.name)}${getTypeParameters(node.typeParameters)} struct {\n${fields.join('\n')}\n}${terminator}`;
|
|
872
1205
|
}
|
|
873
1206
|
else if (isInterfaceDeclaration(node)) {
|
|
874
1207
|
if (options.addFunctionOutside) {
|
|
@@ -889,7 +1222,7 @@ export function visit(node, options = {}) {
|
|
|
889
1222
|
const extendedInterfaces = [];
|
|
890
1223
|
if ((node.heritageClauses ?? [])) {
|
|
891
1224
|
for (const clause of (node.heritageClauses ?? [])) {
|
|
892
|
-
if (clause.token
|
|
1225
|
+
if (clause.token === 'ExtendsKeyword') {
|
|
893
1226
|
for (const type of (clause.types ?? [])) {
|
|
894
1227
|
extendedInterfaces.push(visit(type.expression));
|
|
895
1228
|
}
|
|
@@ -908,7 +1241,7 @@ export function visit(node, options = {}) {
|
|
|
908
1241
|
methods.push(`\t${methodName}(${params})${returnType}`);
|
|
909
1242
|
}
|
|
910
1243
|
else if (isPropertySignature(member) && isIdentifier(member.name)) {
|
|
911
|
-
properties.push(`\t${member.name.text} ${getOptionalNodeType(member.type, !!member.questionToken)}`);
|
|
1244
|
+
properties.push(`\t${goFieldName(member.name.text)} ${getOptionalNodeType(member.type, !!member.questionToken)}`);
|
|
912
1245
|
}
|
|
913
1246
|
}
|
|
914
1247
|
if (properties.length > 0 && methods.length === 0) {
|
|
@@ -933,7 +1266,7 @@ export function visit(node, options = {}) {
|
|
|
933
1266
|
classStaticProps.add(`${className}.${member.name.text}`);
|
|
934
1267
|
}
|
|
935
1268
|
else {
|
|
936
|
-
properties.set(member.name.text,
|
|
1269
|
+
properties.set(member.name.text, getPropertyDeclarationType(member));
|
|
937
1270
|
}
|
|
938
1271
|
}
|
|
939
1272
|
if (isMethodDeclaration(member) && isIdentifier(member.name)) {
|
|
@@ -945,6 +1278,9 @@ export function visit(node, options = {}) {
|
|
|
945
1278
|
}
|
|
946
1279
|
}
|
|
947
1280
|
}
|
|
1281
|
+
for (const param of getParameterProperties(node)) {
|
|
1282
|
+
properties.set(param.name.text, getParameterGoType(param));
|
|
1283
|
+
}
|
|
948
1284
|
classPropertyTypes.set(className, properties);
|
|
949
1285
|
classMethodReturnTypes.set(className, methods);
|
|
950
1286
|
return '';
|
|
@@ -955,7 +1291,7 @@ export function visit(node, options = {}) {
|
|
|
955
1291
|
let parentClass = null;
|
|
956
1292
|
if ((node.heritageClauses ?? [])) {
|
|
957
1293
|
for (const clause of (node.heritageClauses ?? [])) {
|
|
958
|
-
if (clause.token
|
|
1294
|
+
if (clause.token === 'ExtendsKeyword') {
|
|
959
1295
|
parentClass = visit(clause.types[0].expression);
|
|
960
1296
|
}
|
|
961
1297
|
}
|
|
@@ -972,16 +1308,19 @@ export function visit(node, options = {}) {
|
|
|
972
1308
|
fieldType = `[]${getType(member.type, true)}`;
|
|
973
1309
|
}
|
|
974
1310
|
else {
|
|
975
|
-
fieldType =
|
|
1311
|
+
fieldType = getPropertyDeclarationType(member);
|
|
976
1312
|
}
|
|
977
1313
|
fields.push(`\t${fieldName} ${fieldType}`);
|
|
978
1314
|
}
|
|
979
1315
|
}
|
|
1316
|
+
for (const param of getParameterProperties(node)) {
|
|
1317
|
+
fields.push(`\t${goFieldName(param.name.text)} ${getParameterGoType(param)}`);
|
|
1318
|
+
}
|
|
980
1319
|
let result = `type ${name}${typeParams} struct {\n${fields.join('\n')}\n}\n\n`;
|
|
981
1320
|
const ctor = (node.members ?? []).find((m) => isConstructorDeclaration(m));
|
|
982
1321
|
if (ctor) {
|
|
983
1322
|
const ctorParameterInfo = getFunctionParametersInfo(ctor.parameters ?? []);
|
|
984
|
-
const bodyStatements = ctor.body?.statements
|
|
1323
|
+
const bodyStatements = (ctor.body?.statements ?? [])
|
|
985
1324
|
.filter((s) => {
|
|
986
1325
|
if (isExpressionStatement(s) && isCallExpression(s.expression)) {
|
|
987
1326
|
return s.expression.expression.kind !== 'SuperKeyword';
|
|
@@ -990,10 +1329,13 @@ export function visit(node, options = {}) {
|
|
|
990
1329
|
})
|
|
991
1330
|
.map((s) => visit(s))
|
|
992
1331
|
.join('\t') ?? '';
|
|
993
|
-
|
|
1332
|
+
const parameterAssignments = getParameterProperties(node)
|
|
1333
|
+
.map((p) => `self.${goFieldName(p.name.text)} = ${visit(p.name)}\n\t\t`)
|
|
1334
|
+
.join('');
|
|
1335
|
+
result += `func New${name}${typeParams}(${ctorParameterInfo.signature}) *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${ctorParameterInfo.prefixBlockContent}${fieldInitializers(node)}${parameterAssignments}${bodyStatements}return self;\n\t}\n\n`;
|
|
994
1336
|
}
|
|
995
1337
|
else {
|
|
996
|
-
result += `func New${name}${typeParams}() *${name}${typeParamNames} {\n\t\
|
|
1338
|
+
result += `func New${name}${typeParams}() *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${fieldInitializers(node)}return self\n\t}\n\n`;
|
|
997
1339
|
}
|
|
998
1340
|
for (const member of (node.members ?? [])) {
|
|
999
1341
|
if (isMethodDeclaration(member)) {
|
|
@@ -1063,31 +1405,81 @@ export function visit(node, options = {}) {
|
|
|
1063
1405
|
return `New${className}${typeArgs}(${args.join(', ')})`;
|
|
1064
1406
|
}
|
|
1065
1407
|
else if (isObjectLiteralExpression(node)) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1408
|
+
if (isDynamicObjectLiteral(node)) {
|
|
1409
|
+
// { ...a, k: v } → copy a's properties, then set k (in source order)
|
|
1410
|
+
if ((node.properties ?? []).some((p) => p.kind === 'SpreadAssignment')) {
|
|
1411
|
+
useHelper('dynamic');
|
|
1412
|
+
const steps = (node.properties ?? []).map((p) => {
|
|
1413
|
+
if (p.kind === 'SpreadAssignment')
|
|
1414
|
+
return `TnAssign(__obj, ${visit(p.expression)})`;
|
|
1415
|
+
if (isShorthandPropertyAssignment(p))
|
|
1416
|
+
return `__obj[${mapKeyText(p.name)}] = ${visit(p.name)}`;
|
|
1417
|
+
return `__obj[${mapKeyText(p.name)}] = ${visit(p.initializer)}`;
|
|
1418
|
+
});
|
|
1419
|
+
return `func() map[string]interface{} { __obj := map[string]interface{}{}; ${steps.join('; ')}; return __obj }()`;
|
|
1420
|
+
}
|
|
1421
|
+
const entries = (node.properties ?? [])
|
|
1422
|
+
.map((p) => {
|
|
1423
|
+
if (isPropertyAssignment(p))
|
|
1424
|
+
return `${mapKeyText(p.name)}: ${visit(p.initializer)}`;
|
|
1425
|
+
if (isShorthandPropertyAssignment(p))
|
|
1426
|
+
return `${mapKeyText(p.name)}: ${visit(p.name)}`;
|
|
1427
|
+
return '';
|
|
1428
|
+
})
|
|
1429
|
+
.filter((e) => e);
|
|
1430
|
+
return `map[string]interface{}${compositeBody(entries)}`;
|
|
1431
|
+
}
|
|
1432
|
+
const contextualType = resolveTypeNode(getContextualTypeNode(node));
|
|
1433
|
+
if (isRecordTypeNode(contextualType)) {
|
|
1434
|
+
return visitMapLiteral(node, contextualType);
|
|
1435
|
+
}
|
|
1436
|
+
const typeName = contextualType ? getTypeText(contextualType) : '';
|
|
1437
|
+
if (!typeName || typeName === 'interface{}') {
|
|
1438
|
+
return visitAnonymousStructLiteral(node);
|
|
1439
|
+
}
|
|
1440
|
+
// Objects are pointers: the struct type is typeName without its *
|
|
1441
|
+
const structName = typeName.replace(/^\*/, '');
|
|
1442
|
+
// { ...base, x: 1 } → a copy of *base with fields set
|
|
1443
|
+
if ((node.properties ?? []).some((p) => p.kind === 'SpreadAssignment')) {
|
|
1444
|
+
const steps = (node.properties ?? []).map((p) => {
|
|
1445
|
+
if (p.kind === 'SpreadAssignment')
|
|
1446
|
+
return `__obj = *(${visit(p.expression)})`;
|
|
1447
|
+
if (isShorthandPropertyAssignment(p))
|
|
1448
|
+
return `__obj.${goFieldName(p.name.text)} = ${visit(p.name)}`;
|
|
1449
|
+
const spreadFieldType = isIdentifier(p.name)
|
|
1450
|
+
? (interfacePropertyTypes.get(structName)?.get(p.name.text) ??
|
|
1451
|
+
getStructFieldGoType(structName, p.name.text))
|
|
1452
|
+
: undefined;
|
|
1453
|
+
return `__obj.${visit(p.name)} = ${toGoValueOfType(p.initializer, spreadFieldType)}`;
|
|
1454
|
+
});
|
|
1455
|
+
return `func() *${structName} { var __obj ${structName}; ${steps.join('; ')}; return &__obj }()`;
|
|
1069
1456
|
}
|
|
1457
|
+
const fieldTypes = interfacePropertyTypes.get(structName) ?? classPropertyTypes.get(structName);
|
|
1070
1458
|
const properties = (node.properties ?? [])
|
|
1071
1459
|
.map((p) => {
|
|
1072
1460
|
if (isPropertyAssignment(p)) {
|
|
1073
|
-
|
|
1461
|
+
const fieldType = isIdentifier(p.name)
|
|
1462
|
+
? (fieldTypes?.get(p.name.text) ?? getStructFieldGoType(typeName, p.name.text))
|
|
1463
|
+
: undefined;
|
|
1464
|
+
return `${visit(p.name)}: ${toGoValueOfType(p.initializer, fieldType)}`;
|
|
1074
1465
|
}
|
|
1075
1466
|
// Shorthand: { name } → name: name
|
|
1076
1467
|
if (isShorthandPropertyAssignment(p)) {
|
|
1077
|
-
|
|
1078
|
-
return `${name}: ${name}`;
|
|
1468
|
+
return `${goFieldName(p.name.text)}: ${toGoValueOfType(p.name, fieldTypes?.get(p.name.text) ?? getStructFieldGoType(typeName, p.name.text))}`;
|
|
1079
1469
|
}
|
|
1080
1470
|
// Spread: { ...obj } — not easily supported in Go structs, omit
|
|
1081
1471
|
return '';
|
|
1082
1472
|
})
|
|
1083
|
-
.filter((p) => p)
|
|
1084
|
-
|
|
1085
|
-
return `${typeName}{${properties}}`;
|
|
1473
|
+
.filter((p) => p);
|
|
1474
|
+
return `&${structName}${compositeBody(properties)}`;
|
|
1086
1475
|
}
|
|
1087
1476
|
else if (isPropertyAssignment(node)) {
|
|
1088
1477
|
return `${visit(node.name)}: ${visit(node.initializer)}`;
|
|
1089
1478
|
}
|
|
1090
1479
|
else if (isNonNullExpression(node)) {
|
|
1480
|
+
const innerType = inferExpressionType(node.expression);
|
|
1481
|
+
if (innerType && NULLABLE_PRIMITIVE_TYPES.includes(innerType))
|
|
1482
|
+
return `(*${visit(node.expression)})`;
|
|
1091
1483
|
return visit(node.expression);
|
|
1092
1484
|
}
|
|
1093
1485
|
else if (isImportDeclaration(node)) {
|
|
@@ -1097,7 +1489,7 @@ export function visit(node, options = {}) {
|
|
|
1097
1489
|
return '';
|
|
1098
1490
|
}
|
|
1099
1491
|
const syntaxKind = node.kind;
|
|
1100
|
-
if (!['
|
|
1492
|
+
if (!['VariableStatement', 'EndOfFileToken'].includes(syntaxKind)) {
|
|
1101
1493
|
const snippet = String(node.text ?? node.kind).substring(0, 60).replace(/\n/g, ' ');
|
|
1102
1494
|
console.warn(`[TypeNative] Unsupported syntax: ${syntaxKind} — "${snippet}"`);
|
|
1103
1495
|
}
|
|
@@ -1106,6 +1498,850 @@ export function visit(node, options = {}) {
|
|
|
1106
1498
|
}
|
|
1107
1499
|
return code;
|
|
1108
1500
|
}
|
|
1501
|
+
// Renders composite literal entries; multi-line entries need trailing commas
|
|
1502
|
+
// (Go inserts a semicolon after a newline that ends in `}`)
|
|
1503
|
+
// Visits a value going into a slot of Go type `goType`; non-nil values for
|
|
1504
|
+
// nullable primitives (*string/*float64/*bool) are boxed into pointers
|
|
1505
|
+
function toGoValueOfType(expr, goType) {
|
|
1506
|
+
if (goType &&
|
|
1507
|
+
((isObjectLiteralExpression(expr) && (expr.properties ?? []).length === 0 && goType.startsWith('*TnMap[')) ||
|
|
1508
|
+
(isArrayLiteralExpression(expr) && (expr.elements ?? []).length === 0 && goType.startsWith('[]')))) {
|
|
1509
|
+
const mapArgs = splitMapTypeArgs(goType);
|
|
1510
|
+
if (mapArgs)
|
|
1511
|
+
return orderedMapLiteral(mapArgs[0], mapArgs[1], []);
|
|
1512
|
+
return `${goType}{}`;
|
|
1513
|
+
}
|
|
1514
|
+
const code = visit(expr);
|
|
1515
|
+
if (goType && goType !== 'interface{}' && goType !== ':' && isDynamicValue(expr)) {
|
|
1516
|
+
useHelper('dynamic');
|
|
1517
|
+
// T | undefined: box the dynamic value (nil when missing or of another type)
|
|
1518
|
+
if (goType.startsWith('*'))
|
|
1519
|
+
return `TnAsPtr[${goType.slice(1)}](${code})`;
|
|
1520
|
+
return `TnAs[${goType}](${code})`;
|
|
1521
|
+
}
|
|
1522
|
+
// *Struct slot: take the address (shares the value, like a JS object reference)
|
|
1523
|
+
if (goType?.startsWith('*') && isStructGoType(goType.slice(1)) && !isNilLiteral(expr)) {
|
|
1524
|
+
// an untyped literal (&struct{...}) converts to the named pointer type
|
|
1525
|
+
if (isObjectLiteralExpression(expr))
|
|
1526
|
+
return code.startsWith('&struct{') ? `(${goType})(${code})` : code;
|
|
1527
|
+
if (inferExpressionType(expr) !== goType.slice(1))
|
|
1528
|
+
return code;
|
|
1529
|
+
// the element itself (not a bounds-checked copy), so mutations are shared
|
|
1530
|
+
if (isElementAccessExpression(expr) && inferExpressionType(expr.expression)?.startsWith('[]')) {
|
|
1531
|
+
return `&${visit(expr.expression)}[int(${visit(expr.argumentExpression)})]`;
|
|
1532
|
+
}
|
|
1533
|
+
if (isIdentifier(expr) || isPropertyAccessExpression(expr)) {
|
|
1534
|
+
return `&${code}`;
|
|
1535
|
+
}
|
|
1536
|
+
return `func() ${goType} { v := ${code}; return &v }()`;
|
|
1537
|
+
}
|
|
1538
|
+
if (!goType || !NULLABLE_PRIMITIVE_TYPES.includes(goType) || isNilLiteral(expr)) {
|
|
1539
|
+
return code;
|
|
1540
|
+
}
|
|
1541
|
+
if (isConditionalExpression(expr))
|
|
1542
|
+
return code;
|
|
1543
|
+
if (isBinaryExpression(expr) &&
|
|
1544
|
+
expr.operatorToken.kind === 'QuestionQuestionToken' &&
|
|
1545
|
+
inferExpressionType(expr.left)?.startsWith('*')) {
|
|
1546
|
+
return code;
|
|
1547
|
+
}
|
|
1548
|
+
if (inferExpressionType(expr) !== goType.slice(1))
|
|
1549
|
+
return code;
|
|
1550
|
+
return `func() ${goType} { v := ${code}; return &v }()`;
|
|
1551
|
+
}
|
|
1552
|
+
const NULLABLE_PRIMITIVE_TYPES = ['*string', '*float64', '*bool'];
|
|
1553
|
+
// Nullable primitives (T | null → *T) that a guard has narrowed to T in the
|
|
1554
|
+
// code being emitted; reads of them dereference the pointer
|
|
1555
|
+
const narrowedVariables = new Set();
|
|
1556
|
+
function isNullablePrimitiveVariable(name) {
|
|
1557
|
+
return NULLABLE_PRIMITIVE_TYPES.includes(variableGoTypes.get(name) ?? '');
|
|
1558
|
+
}
|
|
1559
|
+
// Narrowing key of a variable or property path (`opts.source`), if it has one
|
|
1560
|
+
function getNarrowingKey(expr) {
|
|
1561
|
+
if (isIdentifier(expr))
|
|
1562
|
+
return expr.text;
|
|
1563
|
+
if (expr.kind === 'ThisKeyword')
|
|
1564
|
+
return 'this';
|
|
1565
|
+
if (isPropertyAccessExpression(expr) && !hasQuestionDot(expr)) {
|
|
1566
|
+
const objectKey = getNarrowingKey(expr.expression);
|
|
1567
|
+
return objectKey ? `${objectKey}.${expr.name.text}` : undefined;
|
|
1568
|
+
}
|
|
1569
|
+
return undefined;
|
|
1570
|
+
}
|
|
1571
|
+
// Emits `emit()` with `names` narrowed
|
|
1572
|
+
function withNarrowing(names, emit) {
|
|
1573
|
+
const added = names.filter((n) => !narrowedVariables.has(n));
|
|
1574
|
+
for (const name of added)
|
|
1575
|
+
narrowedVariables.add(name);
|
|
1576
|
+
const code = emit();
|
|
1577
|
+
for (const name of added)
|
|
1578
|
+
narrowedVariables.delete(name);
|
|
1579
|
+
return code;
|
|
1580
|
+
}
|
|
1581
|
+
function isProcessEnv(expr) {
|
|
1582
|
+
while (isParenthesizedExpression(expr) || isAsExpression(expr) || isNonNullExpression(expr)) {
|
|
1583
|
+
expr = expr.expression;
|
|
1584
|
+
}
|
|
1585
|
+
return (isPropertyAccessExpression(expr) &&
|
|
1586
|
+
isIdentifier(expr.expression) &&
|
|
1587
|
+
expr.expression.text === 'process' &&
|
|
1588
|
+
expr.name.text === 'env');
|
|
1589
|
+
}
|
|
1590
|
+
// Whether an expression is (part of) an optional chain: a?.b, a?.b(), a?.b().c
|
|
1591
|
+
function isOptionalChain(expr) {
|
|
1592
|
+
if (isCallExpression(expr)) {
|
|
1593
|
+
return hasQuestionDot(expr) || isOptionalChain(expr.expression);
|
|
1594
|
+
}
|
|
1595
|
+
if (isPropertyAccessExpression(expr) || isElementAccessExpression(expr)) {
|
|
1596
|
+
return hasQuestionDot(expr) || isOptionalChain(expr.expression);
|
|
1597
|
+
}
|
|
1598
|
+
return false;
|
|
1599
|
+
}
|
|
1600
|
+
// x in `x?.m(...)` / `x?.p`
|
|
1601
|
+
function getOptionalChainBase(expr) {
|
|
1602
|
+
const access = isCallExpression(expr) ? expr.expression : expr;
|
|
1603
|
+
if (isPropertyAccessExpression(access) && hasQuestionDot(access))
|
|
1604
|
+
return access.expression;
|
|
1605
|
+
return undefined;
|
|
1606
|
+
}
|
|
1607
|
+
// Variables known non-null when `condition` evaluates to `whenTrue`
|
|
1608
|
+
function getNarrowedNames(condition, whenTrue) {
|
|
1609
|
+
if (isParenthesizedExpression(condition))
|
|
1610
|
+
return getNarrowedNames(condition.expression, whenTrue);
|
|
1611
|
+
if (whenTrue) {
|
|
1612
|
+
const chainBase = getOptionalChainBase(condition);
|
|
1613
|
+
if (chainBase)
|
|
1614
|
+
return nullableKeys([chainBase]);
|
|
1615
|
+
}
|
|
1616
|
+
if (isIdentifier(condition) || isPropertyAccessExpression(condition)) {
|
|
1617
|
+
return whenTrue ? nullableKeys([condition]) : [];
|
|
1618
|
+
}
|
|
1619
|
+
if (isPrefixUnaryExpression(condition) && condition.operator === 'ExclamationToken') {
|
|
1620
|
+
return getNarrowedNames(condition.operand, !whenTrue);
|
|
1621
|
+
}
|
|
1622
|
+
if (!isBinaryExpression(condition))
|
|
1623
|
+
return [];
|
|
1624
|
+
const kind = condition.operatorToken.kind;
|
|
1625
|
+
if (kind === 'AmpersandAmpersandToken') {
|
|
1626
|
+
return whenTrue
|
|
1627
|
+
? [...getNarrowedNames(condition.left, true), ...getNarrowedNames(condition.right, true)]
|
|
1628
|
+
: [];
|
|
1629
|
+
}
|
|
1630
|
+
if (kind === 'BarBarToken') {
|
|
1631
|
+
return whenTrue
|
|
1632
|
+
? []
|
|
1633
|
+
: [...getNarrowedNames(condition.left, false), ...getNarrowedNames(condition.right, false)];
|
|
1634
|
+
}
|
|
1635
|
+
const isNotEqual = kind === 'ExclamationEqualsEqualsToken' || kind === 'ExclamationEqualsToken';
|
|
1636
|
+
const isEqual = kind === 'EqualsEqualsEqualsToken' || kind === 'EqualsEqualsToken';
|
|
1637
|
+
if (!isNotEqual && !isEqual)
|
|
1638
|
+
return [];
|
|
1639
|
+
const checked = isNilLiteral(condition.right)
|
|
1640
|
+
? condition.left
|
|
1641
|
+
: isNilLiteral(condition.left)
|
|
1642
|
+
? condition.right
|
|
1643
|
+
: undefined;
|
|
1644
|
+
if (!checked) {
|
|
1645
|
+
// x === <non-null value>
|
|
1646
|
+
if (!isEqual || !whenTrue)
|
|
1647
|
+
return [];
|
|
1648
|
+
const leftType = inferExpressionType(condition.left);
|
|
1649
|
+
const rightType = inferExpressionType(condition.right);
|
|
1650
|
+
if (rightType && !NULLABLE_PRIMITIVE_TYPES.includes(rightType))
|
|
1651
|
+
return nullableKeys([condition.left]);
|
|
1652
|
+
if (leftType && !NULLABLE_PRIMITIVE_TYPES.includes(leftType))
|
|
1653
|
+
return nullableKeys([condition.right]);
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
return isNotEqual === whenTrue ? nullableKeys([checked]) : [];
|
|
1657
|
+
}
|
|
1658
|
+
// Narrowing keys of the given expressions that are nullable primitives (*T)
|
|
1659
|
+
function nullableKeys(exprs) {
|
|
1660
|
+
const keys = [];
|
|
1661
|
+
for (const expr of exprs) {
|
|
1662
|
+
const key = getNarrowingKey(expr);
|
|
1663
|
+
if (!key || narrowedVariables.has(key))
|
|
1664
|
+
continue;
|
|
1665
|
+
if (NULLABLE_PRIMITIVE_TYPES.includes(inferExpressionType(expr) ?? ''))
|
|
1666
|
+
keys.push(key);
|
|
1667
|
+
}
|
|
1668
|
+
return keys;
|
|
1669
|
+
}
|
|
1670
|
+
// Identifier occurrences that read the variable (not declarations, member
|
|
1671
|
+
// names, or assignment targets)
|
|
1672
|
+
function isNarrowableReference(node) {
|
|
1673
|
+
const parent = node.parent;
|
|
1674
|
+
if (!parent)
|
|
1675
|
+
return false;
|
|
1676
|
+
if (isPropertyAccessExpression(parent) && parent.name === node)
|
|
1677
|
+
return false;
|
|
1678
|
+
if (parent.name === node)
|
|
1679
|
+
return false;
|
|
1680
|
+
if (isBinaryExpression(parent) && parent.left === node) {
|
|
1681
|
+
const op = parent.operatorToken.kind;
|
|
1682
|
+
if (op === 'EqualsToken' || op === 'QuestionQuestionEqualsToken')
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
function alwaysExits(statement) {
|
|
1688
|
+
if (isReturnStatement(statement) ||
|
|
1689
|
+
isThrowStatement(statement) ||
|
|
1690
|
+
isBreakStatement(statement) ||
|
|
1691
|
+
statement.kind === 'ContinueStatement') {
|
|
1692
|
+
return true;
|
|
1693
|
+
}
|
|
1694
|
+
if (isBlock(statement)) {
|
|
1695
|
+
const statements = statement.statements ?? [];
|
|
1696
|
+
return statements.length > 0 && alwaysExits(statements[statements.length - 1]);
|
|
1697
|
+
}
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
// Statements of a block; after `if (!x) return;`, x stays narrowed for the rest
|
|
1701
|
+
function visitBlockStatements(statements) {
|
|
1702
|
+
const added = [];
|
|
1703
|
+
const parts = [];
|
|
1704
|
+
for (const statement of statements) {
|
|
1705
|
+
parts.push(visit(statement));
|
|
1706
|
+
if (isIfStatement(statement) && !statement.elseStatement && alwaysExits(statement.thenStatement)) {
|
|
1707
|
+
for (const name of getNarrowedNames(statement.expression, false)) {
|
|
1708
|
+
if (!narrowedVariables.has(name)) {
|
|
1709
|
+
narrowedVariables.add(name);
|
|
1710
|
+
added.push(name);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
for (const name of added)
|
|
1716
|
+
narrowedVariables.delete(name);
|
|
1717
|
+
return parts.join('\t');
|
|
1718
|
+
}
|
|
1719
|
+
function visitDynamicArithmetic(node, op) {
|
|
1720
|
+
const leftDynamic = isDynamicValue(node.left);
|
|
1721
|
+
const rightDynamic = isDynamicValue(node.right);
|
|
1722
|
+
if (!leftDynamic && !rightDynamic)
|
|
1723
|
+
return undefined;
|
|
1724
|
+
const isConcat = op === '+' && [node.left, node.right].some((side) => inferExpressionType(side) === 'string');
|
|
1725
|
+
const operand = (side, dynamic) => {
|
|
1726
|
+
if (!dynamic)
|
|
1727
|
+
return visit(side);
|
|
1728
|
+
if (isConcat) {
|
|
1729
|
+
importedPackages.add('fmt');
|
|
1730
|
+
return `fmt.Sprint(${visit(side)})`;
|
|
1731
|
+
}
|
|
1732
|
+
useHelper('dynamic');
|
|
1733
|
+
return `TnAs[float64](${visit(side)})`;
|
|
1734
|
+
};
|
|
1735
|
+
return `${operand(node.left, leftDynamic)} ${op} ${operand(node.right, rightDynamic)}`;
|
|
1736
|
+
}
|
|
1737
|
+
// `x === v` where x is *T and v is T: equal only when x is non-nil and *x == v
|
|
1738
|
+
function visitNullableComparison(node, op) {
|
|
1739
|
+
const leftType = inferExpressionType(node.left);
|
|
1740
|
+
const rightType = inferExpressionType(node.right);
|
|
1741
|
+
let pointerSide;
|
|
1742
|
+
let valueSide;
|
|
1743
|
+
if (leftType && NULLABLE_PRIMITIVE_TYPES.includes(leftType) && rightType === leftType.slice(1)) {
|
|
1744
|
+
pointerSide = node.left;
|
|
1745
|
+
valueSide = node.right;
|
|
1746
|
+
}
|
|
1747
|
+
else if (rightType &&
|
|
1748
|
+
NULLABLE_PRIMITIVE_TYPES.includes(rightType) &&
|
|
1749
|
+
leftType === rightType.slice(1)) {
|
|
1750
|
+
pointerSide = node.right;
|
|
1751
|
+
valueSide = node.left;
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
return undefined;
|
|
1755
|
+
}
|
|
1756
|
+
const tmp = getTempName('cmp');
|
|
1757
|
+
const equal = `${tmp} != nil && *${tmp} == ${visit(valueSide)}`;
|
|
1758
|
+
return `func() bool { ${tmp} := ${visit(pointerSide)}; return ${op === '==' ? equal : `!(${equal})`} }()`;
|
|
1759
|
+
}
|
|
1760
|
+
// Go type of a Map/Set/Record: an insertion-ordered map
|
|
1761
|
+
function mapGoType(keyType, valueType) {
|
|
1762
|
+
useHelper('orderedMap');
|
|
1763
|
+
return `*TnMap[${keyType}, ${valueType}]`;
|
|
1764
|
+
}
|
|
1765
|
+
// [K, V] of a *TnMap[K, V] type string
|
|
1766
|
+
function splitMapTypeArgs(mapType) {
|
|
1767
|
+
if (!mapType.startsWith('*TnMap['))
|
|
1768
|
+
return undefined;
|
|
1769
|
+
const body = mapType.slice(7, -1);
|
|
1770
|
+
let depth = 0;
|
|
1771
|
+
for (let i = 0; i < body.length; i++) {
|
|
1772
|
+
const ch = body[i];
|
|
1773
|
+
if (ch === '[' || ch === '(' || ch === '{')
|
|
1774
|
+
depth++;
|
|
1775
|
+
else if (ch === ']' || ch === ')' || ch === '}')
|
|
1776
|
+
depth--;
|
|
1777
|
+
else if (ch === ',' && depth === 0)
|
|
1778
|
+
return [body.slice(0, i).trim(), body.slice(i + 1).trim()];
|
|
1779
|
+
}
|
|
1780
|
+
return undefined;
|
|
1781
|
+
}
|
|
1782
|
+
// Key type of a map type string: *TnMap[K, V] → K
|
|
1783
|
+
function extractMapKeyType(mapType) {
|
|
1784
|
+
const args = splitMapTypeArgs(mapType);
|
|
1785
|
+
if (args)
|
|
1786
|
+
return args[0];
|
|
1787
|
+
let depth = 0;
|
|
1788
|
+
for (let i = 4; i < mapType.length; i++) {
|
|
1789
|
+
if (mapType[i] === '[')
|
|
1790
|
+
depth++;
|
|
1791
|
+
else if (mapType[i] === ']') {
|
|
1792
|
+
if (depth === 0)
|
|
1793
|
+
return mapType.slice(4, i);
|
|
1794
|
+
depth--;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return 'interface{}';
|
|
1798
|
+
}
|
|
1799
|
+
// Field type of an anonymous struct type string: struct{ a T; b U } → field b → U
|
|
1800
|
+
function getStructFieldGoType(structType, field) {
|
|
1801
|
+
structType = structType.replace(/^\*/, '');
|
|
1802
|
+
if (!structType.startsWith('struct{'))
|
|
1803
|
+
return undefined;
|
|
1804
|
+
const body = structType.slice(7, structType.lastIndexOf('}'));
|
|
1805
|
+
let depth = 0;
|
|
1806
|
+
let start = 0;
|
|
1807
|
+
const fields = [];
|
|
1808
|
+
for (let i = 0; i <= body.length; i++) {
|
|
1809
|
+
const ch = i < body.length ? body[i] : ';';
|
|
1810
|
+
if (ch === '{' || ch === '(' || ch === '[')
|
|
1811
|
+
depth++;
|
|
1812
|
+
else if (ch === '}' || ch === ')' || ch === ']')
|
|
1813
|
+
depth--;
|
|
1814
|
+
else if (ch === ';' && depth === 0) {
|
|
1815
|
+
fields.push(body.slice(start, i).trim());
|
|
1816
|
+
start = i + 1;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
for (const entry of fields) {
|
|
1820
|
+
const space = entry.indexOf(' ');
|
|
1821
|
+
if (space > 0 && entry.slice(0, space) === goFieldName(field))
|
|
1822
|
+
return entry.slice(space + 1).trim();
|
|
1823
|
+
}
|
|
1824
|
+
return undefined;
|
|
1825
|
+
}
|
|
1826
|
+
// A Go expression as the string JS would produce for it (numbers like JS:
|
|
1827
|
+
// 1000000, 0.5, NaN; not Go's 1e+06)
|
|
1828
|
+
function jsStringOf(code, goType) {
|
|
1829
|
+
if (goType === 'string')
|
|
1830
|
+
return code;
|
|
1831
|
+
if (goType === 'float64') {
|
|
1832
|
+
useHelper('dynamic');
|
|
1833
|
+
return `TnNumStr(${code})`;
|
|
1834
|
+
}
|
|
1835
|
+
if (goType === 'bool') {
|
|
1836
|
+
importedPackages.add('strconv');
|
|
1837
|
+
return `strconv.FormatBool(${code})`;
|
|
1838
|
+
}
|
|
1839
|
+
importedPackages.add('fmt');
|
|
1840
|
+
return `fmt.Sprintf("%v", ${code})`;
|
|
1841
|
+
}
|
|
1842
|
+
// JS bitwise operators work on 32-bit integers (ToInt32 / ToUint32)
|
|
1843
|
+
function visitBitwise(node, op) {
|
|
1844
|
+
const kind = node.operatorToken.kind;
|
|
1845
|
+
const base = {
|
|
1846
|
+
AmpersandToken: '&',
|
|
1847
|
+
BarToken: '|',
|
|
1848
|
+
CaretToken: '^',
|
|
1849
|
+
LessThanLessThanToken: '<<',
|
|
1850
|
+
GreaterThanGreaterThanToken: '>>',
|
|
1851
|
+
GreaterThanGreaterThanGreaterThanToken: '>>>',
|
|
1852
|
+
AmpersandEqualsToken: '&',
|
|
1853
|
+
BarEqualsToken: '|',
|
|
1854
|
+
CaretEqualsToken: '^',
|
|
1855
|
+
LessThanLessThanEqualsToken: '<<',
|
|
1856
|
+
GreaterThanGreaterThanEqualsToken: '>>',
|
|
1857
|
+
GreaterThanGreaterThanGreaterThanEqualsToken: '>>>'
|
|
1858
|
+
};
|
|
1859
|
+
const bitOp = base[kind];
|
|
1860
|
+
if (!bitOp)
|
|
1861
|
+
return undefined;
|
|
1862
|
+
useHelper('dynamic');
|
|
1863
|
+
const left = visit(node.left);
|
|
1864
|
+
const right = visit(node.right);
|
|
1865
|
+
let value;
|
|
1866
|
+
if (bitOp === '>>>')
|
|
1867
|
+
value = `float64(TnUint32(${left}) >> (TnUint32(${right}) & 31))`;
|
|
1868
|
+
else if (bitOp === '<<' || bitOp === '>>') {
|
|
1869
|
+
value = `float64(TnInt32(${left}) ${bitOp} (TnUint32(${right}) & 31))`;
|
|
1870
|
+
}
|
|
1871
|
+
else
|
|
1872
|
+
value = `float64(TnInt32(${left}) ${bitOp} TnInt32(${right}))`;
|
|
1873
|
+
const isAssignment = kind.endsWith('EqualsToken');
|
|
1874
|
+
return isAssignment ? `${left} = ${value}` : value;
|
|
1875
|
+
}
|
|
1876
|
+
// delete obj.key / delete obj[key] on maps (Records and dynamic objects)
|
|
1877
|
+
function visitDelete(node) {
|
|
1878
|
+
const target = unwrapParentheses(node.expression);
|
|
1879
|
+
const object = target.expression;
|
|
1880
|
+
const key = isPropertyAccessExpression(target)
|
|
1881
|
+
? toGoStringLiteral(target.name.text)
|
|
1882
|
+
: visit(target.argumentExpression);
|
|
1883
|
+
const objectType = inferExpressionType(object);
|
|
1884
|
+
if (objectType?.startsWith('*TnMap['))
|
|
1885
|
+
return `${visit(object)}.Delete(${key})`;
|
|
1886
|
+
useHelper('dynamic');
|
|
1887
|
+
return `TnDelete(${visit(object)}, ${key})`;
|
|
1888
|
+
}
|
|
1889
|
+
// f(...xs): variadic callees take xs...; fixed-parameter functions get
|
|
1890
|
+
// xs[0], xs[1], … for their remaining parameters
|
|
1891
|
+
function visitSpreadCallArguments(node) {
|
|
1892
|
+
const fn = isIdentifier(node.expression) ? declaredFunctions.get(node.expression.text) : undefined;
|
|
1893
|
+
const params = fn?.parameters ?? [];
|
|
1894
|
+
const isVariadic = !fn || params.some((p) => p.dotDotDotToken);
|
|
1895
|
+
const result = [];
|
|
1896
|
+
for (const arg of node.arguments ?? []) {
|
|
1897
|
+
if (!isSpreadElement(arg)) {
|
|
1898
|
+
result.push(visit(arg));
|
|
1899
|
+
}
|
|
1900
|
+
else if (isVariadic) {
|
|
1901
|
+
result.push(`${visit(arg.expression)}...`);
|
|
1902
|
+
}
|
|
1903
|
+
else {
|
|
1904
|
+
useHelper('dynamic');
|
|
1905
|
+
const source = visit(arg.expression);
|
|
1906
|
+
const start = result.length;
|
|
1907
|
+
for (let i = start; i < params.length; i++)
|
|
1908
|
+
result.push(`TnAt(${source}, ${i - start})`);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return result;
|
|
1912
|
+
}
|
|
1913
|
+
// self.field = initializer for each instance field that has one
|
|
1914
|
+
function fieldInitializers(classNode) {
|
|
1915
|
+
return (classNode.members ?? [])
|
|
1916
|
+
.filter((m) => isPropertyDeclaration(m) &&
|
|
1917
|
+
m.initializer &&
|
|
1918
|
+
isIdentifier(m.name) &&
|
|
1919
|
+
!(m.modifiers ?? []).some((mod) => mod.kind === 'StaticKeyword'))
|
|
1920
|
+
.map((m) => `self.${goFieldName(m.name.text)} = ${toGoValueOfType(m.initializer, getPropertyDeclarationType(m))}\n\t\t`)
|
|
1921
|
+
.join('');
|
|
1922
|
+
}
|
|
1923
|
+
// A class field's type: declared, else inferred from its initializer (n = 1 → float64)
|
|
1924
|
+
function getPropertyDeclarationType(member) {
|
|
1925
|
+
if (member.type || !member.initializer)
|
|
1926
|
+
return getOptionalNodeType(member.type, !!member.questionToken);
|
|
1927
|
+
return toFieldGoType(inferExpressionType(member.initializer));
|
|
1928
|
+
}
|
|
1929
|
+
function isIncrementOrDecrement(node) {
|
|
1930
|
+
return node.operator === 'PlusPlusToken' || node.operator === 'MinusMinusToken';
|
|
1931
|
+
}
|
|
1932
|
+
// x++ / ++x: Go only has the statement form, so used as a value they become
|
|
1933
|
+
// a closure returning the old (postfix) or new (prefix) value
|
|
1934
|
+
function visitIncrementOrDecrement(node, isPrefix) {
|
|
1935
|
+
const target = visit(node.operand, { inline: true });
|
|
1936
|
+
const op = node.operator === 'PlusPlusToken' ? '++' : '--';
|
|
1937
|
+
const parent = node.parent;
|
|
1938
|
+
const isStatement = isExpressionStatement(parent) || (isForStatement(parent) && parent.incrementor === node);
|
|
1939
|
+
if (isStatement)
|
|
1940
|
+
return `${target}${op}`;
|
|
1941
|
+
if (isPrefix)
|
|
1942
|
+
return `func() float64 { ${target}${op}; return ${target} }()`;
|
|
1943
|
+
return `func() float64 { __old := ${target}; ${target}${op}; return __old }()`;
|
|
1944
|
+
}
|
|
1945
|
+
function compositeBody(rawEntries) {
|
|
1946
|
+
const entries = rawEntries.map((e) => e.trimEnd());
|
|
1947
|
+
if (!entries.some((e) => e.includes('\n')))
|
|
1948
|
+
return `{${entries.join(', ')}}`;
|
|
1949
|
+
return `{\n\t${entries.join(',\n\t')},\n}`;
|
|
1950
|
+
}
|
|
1951
|
+
// Struct field type for an inferred value type (null/unknown → interface{})
|
|
1952
|
+
function toFieldGoType(goType) {
|
|
1953
|
+
return !goType || goType === 'nil' ? 'interface{}' : goType;
|
|
1954
|
+
}
|
|
1955
|
+
function isDictionaryLiteral(node) {
|
|
1956
|
+
const declaration = node.parent;
|
|
1957
|
+
if (!isVariableDeclaration(declaration) || !isIdentifier(declaration.name))
|
|
1958
|
+
return false;
|
|
1959
|
+
let scope = declaration.parent;
|
|
1960
|
+
while (scope && !isBlock(scope) && !isSourceFile(scope))
|
|
1961
|
+
scope = scope.parent;
|
|
1962
|
+
if (!scope)
|
|
1963
|
+
return false;
|
|
1964
|
+
const name = declaration.name.text;
|
|
1965
|
+
return containsNode(scope, (n) => isElementAccessExpression(n) &&
|
|
1966
|
+
isIdentifier(n.expression) &&
|
|
1967
|
+
n.expression.text === name &&
|
|
1968
|
+
!isStringLiteral(n.argumentExpression));
|
|
1969
|
+
}
|
|
1970
|
+
function containsNode(root, predicate) {
|
|
1971
|
+
for (const child of childNodes(root)) {
|
|
1972
|
+
if (predicate(child) || containsNode(child, predicate))
|
|
1973
|
+
return true;
|
|
1974
|
+
}
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
function getDictionaryValueType(node) {
|
|
1978
|
+
let valueType;
|
|
1979
|
+
for (const p of node.properties ?? []) {
|
|
1980
|
+
if (!isPropertyAssignment(p))
|
|
1981
|
+
continue;
|
|
1982
|
+
const t = inferExpressionType(p.initializer);
|
|
1983
|
+
if (!t || (valueType && t !== valueType))
|
|
1984
|
+
return 'interface{}';
|
|
1985
|
+
valueType = t;
|
|
1986
|
+
}
|
|
1987
|
+
return valueType ?? 'interface{}';
|
|
1988
|
+
}
|
|
1989
|
+
// Go type of an object literal, matching what the visitor emits
|
|
1990
|
+
// Object literals that are dynamic objects: in an any context, or spreading an any value
|
|
1991
|
+
function isDynamicObjectLiteral(node) {
|
|
1992
|
+
if (isAnyContext(getContextualTypeNode(node)))
|
|
1993
|
+
return true;
|
|
1994
|
+
return (node.properties ?? []).some((p) => p.kind === 'SpreadAssignment' && isDynamicValue(p.expression));
|
|
1995
|
+
}
|
|
1996
|
+
function getObjectLiteralGoType(node) {
|
|
1997
|
+
if (isDynamicObjectLiteral(node))
|
|
1998
|
+
return 'interface{}';
|
|
1999
|
+
const contextualType = resolveTypeNode(getContextualTypeNode(node));
|
|
2000
|
+
if (isRecordTypeNode(contextualType))
|
|
2001
|
+
return getType(contextualType);
|
|
2002
|
+
const typeName = contextualType ? getType(contextualType) : '';
|
|
2003
|
+
if (typeName && typeName !== 'interface{}' && typeName !== ':')
|
|
2004
|
+
return typeName;
|
|
2005
|
+
if (isDictionaryLiteral(node))
|
|
2006
|
+
return mapGoType('string', getDictionaryValueType(node));
|
|
2007
|
+
const fields = (node.properties ?? [])
|
|
2008
|
+
.filter((p) => (isPropertyAssignment(p) || isShorthandPropertyAssignment(p)) && isIdentifier(p.name))
|
|
2009
|
+
.map((p) => {
|
|
2010
|
+
const value = isPropertyAssignment(p) ? p.initializer : p.name;
|
|
2011
|
+
return `${goFieldName(p.name.text)} ${toFieldGoType(inferExpressionType(value))}`;
|
|
2012
|
+
});
|
|
2013
|
+
return `*struct{ ${fields.join('; ')} }`;
|
|
2014
|
+
}
|
|
2015
|
+
// Values typed `any`/`unknown` are handled at runtime (TnGet/TnSet/...),
|
|
2016
|
+
// never by guessing their type
|
|
2017
|
+
function isDynamicValue(expr) {
|
|
2018
|
+
return inferExpressionType(expr) === 'interface{}';
|
|
2019
|
+
}
|
|
2020
|
+
// Whether an expression is written to (x = …, x += …, x++)
|
|
2021
|
+
function isAssignmentTarget(node) {
|
|
2022
|
+
const parent = node.parent;
|
|
2023
|
+
if (isBinaryExpression(parent) && parent.left === node) {
|
|
2024
|
+
return /^(EqualsToken|.*EqualsToken)$/.test(parent.operatorToken.kind) &&
|
|
2025
|
+
!['EqualsEqualsToken', 'EqualsEqualsEqualsToken', 'ExclamationEqualsToken', 'ExclamationEqualsEqualsToken', 'LessThanEqualsToken', 'GreaterThanEqualsToken'].includes(parent.operatorToken.kind);
|
|
2026
|
+
}
|
|
2027
|
+
return ((isPrefixUnaryExpression(parent) || isPostfixUnaryExpression(parent)) &&
|
|
2028
|
+
['PlusPlusToken', 'MinusMinusToken'].includes(parent.operator));
|
|
2029
|
+
}
|
|
2030
|
+
// `x as T`: an any value cast to an object type stays dynamic (a JS object
|
|
2031
|
+
// cannot become a Go struct); other casts convert to T
|
|
2032
|
+
function getAsExpressionType(node) {
|
|
2033
|
+
const target = getType(node.type);
|
|
2034
|
+
if (isDynamicValue(node.expression) && (isStructGoType(target) || target.startsWith('*'))) {
|
|
2035
|
+
return 'interface{}';
|
|
2036
|
+
}
|
|
2037
|
+
return target;
|
|
2038
|
+
}
|
|
2039
|
+
function isCallee(node) {
|
|
2040
|
+
return isCallExpression(node.parent) && node.parent.expression === node;
|
|
2041
|
+
}
|
|
2042
|
+
// Whether a type node is `any`/`unknown`, directly or through aliases
|
|
2043
|
+
function isAnyContext(typeNode) {
|
|
2044
|
+
for (let depth = 0; typeNode && depth < 10; depth++) {
|
|
2045
|
+
if (isAnyTypeNode(typeNode))
|
|
2046
|
+
return true;
|
|
2047
|
+
// any | undefined is still any
|
|
2048
|
+
if (isUnionTypeNode(typeNode)) {
|
|
2049
|
+
return (typeNode.types ?? []).some((t) => isAnyContext(t));
|
|
2050
|
+
}
|
|
2051
|
+
if (!isTypeReferenceNode(typeNode) || !isIdentifier(typeNode.typeName))
|
|
2052
|
+
return false;
|
|
2053
|
+
typeNode = declaredTypeAliases.get(typeNode.typeName.text);
|
|
2054
|
+
}
|
|
2055
|
+
return false;
|
|
2056
|
+
}
|
|
2057
|
+
function isRecordTypeNode(typeNode) {
|
|
2058
|
+
return (isTypeReferenceNode(typeNode) &&
|
|
2059
|
+
isIdentifier(typeNode.typeName) &&
|
|
2060
|
+
typeNode.typeName.text === 'Record' &&
|
|
2061
|
+
(typeNode.typeArguments ?? []).length === 2);
|
|
2062
|
+
}
|
|
2063
|
+
// Object literal keys as Go map keys: identifiers become string literals
|
|
2064
|
+
function mapKeyText(name) {
|
|
2065
|
+
if (isIdentifier(name))
|
|
2066
|
+
return toGoStringLiteral(name.text);
|
|
2067
|
+
if (name.kind === 'ComputedPropertyName')
|
|
2068
|
+
return visit(name.expression);
|
|
2069
|
+
return visit(name);
|
|
2070
|
+
}
|
|
2071
|
+
// { a: 1 } typed as Record<K, V> → map[K]V{"a": 1}
|
|
2072
|
+
// An ordered map built from "key, value" entries (in source order)
|
|
2073
|
+
function orderedMapLiteral(keyType, valueType, entries) {
|
|
2074
|
+
useHelper('orderedMap');
|
|
2075
|
+
const constructor = `TnNewMap[${keyType}, ${valueType}]()`;
|
|
2076
|
+
if (entries.length === 0)
|
|
2077
|
+
return constructor;
|
|
2078
|
+
const sets = entries.map((e) => `.Set(${e})`).join('');
|
|
2079
|
+
return `${constructor}${sets}`;
|
|
2080
|
+
}
|
|
2081
|
+
function visitMapLiteral(node, recordType) {
|
|
2082
|
+
const keyType = getType(recordType.typeArguments[0]);
|
|
2083
|
+
const valueType = getType(recordType.typeArguments[1]);
|
|
2084
|
+
const entries = [];
|
|
2085
|
+
for (const p of node.properties ?? []) {
|
|
2086
|
+
if (isPropertyAssignment(p))
|
|
2087
|
+
entries.push(`${mapKeyText(p.name)}, ${toGoValueOfType(p.initializer, valueType)}`);
|
|
2088
|
+
else if (isShorthandPropertyAssignment(p))
|
|
2089
|
+
entries.push(`${mapKeyText(p.name)}, ${visit(p.name)}`);
|
|
2090
|
+
}
|
|
2091
|
+
return orderedMapLiteral(keyType, valueType, entries);
|
|
2092
|
+
}
|
|
2093
|
+
// Object literal with no contextual type → anonymous struct with inferred fields,
|
|
2094
|
+
// or a map when the variable holding it is indexed dynamically (obj[key])
|
|
2095
|
+
function visitAnonymousStructLiteral(node) {
|
|
2096
|
+
if (isDictionaryLiteral(node)) {
|
|
2097
|
+
const valueType = getDictionaryValueType(node);
|
|
2098
|
+
const entries = (node.properties ?? [])
|
|
2099
|
+
.filter((p) => isPropertyAssignment(p))
|
|
2100
|
+
.map((p) => `${mapKeyText(p.name)}, ${visit(p.initializer)}`);
|
|
2101
|
+
return orderedMapLiteral('string', valueType, entries);
|
|
2102
|
+
}
|
|
2103
|
+
const fields = [];
|
|
2104
|
+
const values = [];
|
|
2105
|
+
for (const p of node.properties ?? []) {
|
|
2106
|
+
if (isPropertyAssignment(p) && isIdentifier(p.name)) {
|
|
2107
|
+
const field = goFieldName(p.name.text);
|
|
2108
|
+
fields.push(`${field} ${toFieldGoType(inferExpressionType(p.initializer))}`);
|
|
2109
|
+
values.push(`${field}: ${visit(p.initializer)}`);
|
|
2110
|
+
}
|
|
2111
|
+
else if (isShorthandPropertyAssignment(p)) {
|
|
2112
|
+
const field = goFieldName(p.name.text);
|
|
2113
|
+
fields.push(`${field} ${toFieldGoType(inferExpressionType(p.name))}`);
|
|
2114
|
+
values.push(`${field}: ${visit(p.name)}`);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return `&struct{ ${fields.join('; ')} }${compositeBody(values)}`;
|
|
2118
|
+
}
|
|
2119
|
+
// Follows type aliases and strips null/undefined from unions
|
|
2120
|
+
function resolveTypeNode(typeNode, depth = 0) {
|
|
2121
|
+
if (!typeNode || depth > 10)
|
|
2122
|
+
return typeNode;
|
|
2123
|
+
if (isAnyTypeNode(typeNode))
|
|
2124
|
+
return undefined;
|
|
2125
|
+
if (typeNode.kind === 'ParenthesizedType')
|
|
2126
|
+
return resolveTypeNode(typeNode.type, depth + 1);
|
|
2127
|
+
if (isUnionTypeNode(typeNode)) {
|
|
2128
|
+
const nonNull = (typeNode.types ?? []).filter((t) => t.kind !== 'NullKeyword' &&
|
|
2129
|
+
t.kind !== 'UndefinedKeyword' &&
|
|
2130
|
+
!(isLiteralTypeNode(t) && t.literal?.kind === 'NullKeyword'));
|
|
2131
|
+
return nonNull.length === 1 ? resolveTypeNode(nonNull[0], depth + 1) : typeNode;
|
|
2132
|
+
}
|
|
2133
|
+
if (isTypeReferenceNode(typeNode) && isIdentifier(typeNode.typeName)) {
|
|
2134
|
+
const alias = declaredTypeAliases.get(typeNode.typeName.text);
|
|
2135
|
+
if (alias && alias.kind !== 'TypeLiteral')
|
|
2136
|
+
return resolveTypeNode(alias, depth + 1);
|
|
2137
|
+
}
|
|
2138
|
+
return typeNode;
|
|
2139
|
+
}
|
|
2140
|
+
function getEnclosingFunction(node) {
|
|
2141
|
+
let current = node.parent;
|
|
2142
|
+
while (current) {
|
|
2143
|
+
if (isFunctionDeclaration(current) ||
|
|
2144
|
+
isFunctionExpression(current) ||
|
|
2145
|
+
isArrowFunction(current) ||
|
|
2146
|
+
isMethodDeclaration(current) ||
|
|
2147
|
+
isGetAccessor(current)) {
|
|
2148
|
+
return current;
|
|
2149
|
+
}
|
|
2150
|
+
current = current.parent;
|
|
2151
|
+
}
|
|
2152
|
+
return undefined;
|
|
2153
|
+
}
|
|
2154
|
+
// Declared return type of a function, unwrapping Promise<T> for async functions
|
|
2155
|
+
function getReturnTypeNode(fn) {
|
|
2156
|
+
const typeNode = fn?.type ?? (fn ? getContextualFunctionType(fn)?.type : undefined);
|
|
2157
|
+
if (isTypeReferenceNode(typeNode) &&
|
|
2158
|
+
isIdentifier(typeNode.typeName) &&
|
|
2159
|
+
typeNode.typeName.text === 'Promise') {
|
|
2160
|
+
return typeNode.typeArguments?.[0];
|
|
2161
|
+
}
|
|
2162
|
+
return typeNode;
|
|
2163
|
+
}
|
|
2164
|
+
// Type of member `name` within an object type (interface, type literal, Record)
|
|
2165
|
+
function getMemberTypeNode(typeNode, name) {
|
|
2166
|
+
const resolved = resolveTypeNode(typeNode);
|
|
2167
|
+
if (!resolved)
|
|
2168
|
+
return undefined;
|
|
2169
|
+
if (isRecordTypeNode(resolved))
|
|
2170
|
+
return resolved.typeArguments[1];
|
|
2171
|
+
let members = [];
|
|
2172
|
+
if (resolved.kind === 'TypeLiteral') {
|
|
2173
|
+
members = resolved.members ?? [];
|
|
2174
|
+
}
|
|
2175
|
+
else if (isTypeReferenceNode(resolved) && isIdentifier(resolved.typeName)) {
|
|
2176
|
+
const aliasLiteral = declaredTypeAliases.get(resolved.typeName.text);
|
|
2177
|
+
if (aliasLiteral?.kind === 'TypeLiteral')
|
|
2178
|
+
members = aliasLiteral.members ?? [];
|
|
2179
|
+
const iface = declaredInterfaces.get(resolved.typeName.text);
|
|
2180
|
+
if (iface) {
|
|
2181
|
+
members = iface.members ?? [];
|
|
2182
|
+
for (const clause of iface.heritageClauses ?? []) {
|
|
2183
|
+
for (const base of clause.types ?? []) {
|
|
2184
|
+
const baseType = { kind: 'TypeReference', typeName: base.expression };
|
|
2185
|
+
const inherited = getMemberTypeNode(baseType, name);
|
|
2186
|
+
if (inherited)
|
|
2187
|
+
return inherited;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
for (const member of members) {
|
|
2193
|
+
if (isPropertySignature(member) && isIdentifier(member.name) && member.name.text === name) {
|
|
2194
|
+
return member.type;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return undefined;
|
|
2198
|
+
}
|
|
2199
|
+
// The type an expression is expected to have from where it appears
|
|
2200
|
+
// (TypeScript's "contextual type"), derived syntactically from declarations
|
|
2201
|
+
// Declared type nodes of variables/parameters in scope (latest declaration wins)
|
|
2202
|
+
const variableTypeNodes = new Map();
|
|
2203
|
+
// Declared type (as a type node) of an assignable expression, when known
|
|
2204
|
+
function getExpressionTypeNode(expr) {
|
|
2205
|
+
if (isIdentifier(expr))
|
|
2206
|
+
return variableTypeNodes.get(expr.text);
|
|
2207
|
+
if (isParenthesizedExpression(expr))
|
|
2208
|
+
return getExpressionTypeNode(expr.expression);
|
|
2209
|
+
if (isPropertyAccessExpression(expr)) {
|
|
2210
|
+
if (expr.expression.kind === 'ThisKeyword') {
|
|
2211
|
+
let current = expr.parent;
|
|
2212
|
+
while (current && !isClassDeclaration(current))
|
|
2213
|
+
current = current.parent;
|
|
2214
|
+
const member = (current?.members ?? []).find((m) => isPropertyDeclaration(m) && isIdentifier(m.name) && m.name.text === expr.name.text);
|
|
2215
|
+
return member?.type;
|
|
2216
|
+
}
|
|
2217
|
+
const objectType = getExpressionTypeNode(expr.expression);
|
|
2218
|
+
return objectType ? getMemberTypeNode(objectType, expr.name.text) : undefined;
|
|
2219
|
+
}
|
|
2220
|
+
if (isElementAccessExpression(expr)) {
|
|
2221
|
+
const objectType = resolveTypeNode(getExpressionTypeNode(expr.expression));
|
|
2222
|
+
if (isArrayTypeNode(objectType))
|
|
2223
|
+
return objectType.elementType;
|
|
2224
|
+
if (isTypeReferenceNode(objectType) &&
|
|
2225
|
+
isIdentifier(objectType.typeName) &&
|
|
2226
|
+
['Record', 'Map'].includes(objectType.typeName.text)) {
|
|
2227
|
+
return objectType.typeArguments?.[1];
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
return undefined;
|
|
2231
|
+
}
|
|
2232
|
+
function getContextualTypeNode(node) {
|
|
2233
|
+
const parent = node.parent;
|
|
2234
|
+
if (!parent)
|
|
2235
|
+
return undefined;
|
|
2236
|
+
if (isParenthesizedExpression(parent))
|
|
2237
|
+
return getContextualTypeNode(parent);
|
|
2238
|
+
if (isConditionalExpression(parent) && parent.condition !== node) {
|
|
2239
|
+
return getContextualTypeNode(parent);
|
|
2240
|
+
}
|
|
2241
|
+
if (isBinaryExpression(parent)) {
|
|
2242
|
+
const op = parent.operatorToken.kind;
|
|
2243
|
+
if ((op === 'QuestionQuestionToken' || op === 'BarBarToken') && parent.right === node) {
|
|
2244
|
+
return getContextualTypeNode(parent);
|
|
2245
|
+
}
|
|
2246
|
+
if (op === 'EqualsToken' && parent.right === node) {
|
|
2247
|
+
return getExpressionTypeNode(parent.left);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
if (isAsExpression(parent) || isTypeAssertionExpression(parent))
|
|
2251
|
+
return parent.type;
|
|
2252
|
+
if ((isVariableDeclaration(parent) || isPropertyDeclaration(parent) || parent.kind === 'Parameter') &&
|
|
2253
|
+
parent.initializer === node) {
|
|
2254
|
+
return parent.type;
|
|
2255
|
+
}
|
|
2256
|
+
if (isReturnStatement(parent)) {
|
|
2257
|
+
return getReturnTypeNode(getEnclosingFunction(parent));
|
|
2258
|
+
}
|
|
2259
|
+
if (isArrowFunction(parent) && parent.body === node) {
|
|
2260
|
+
return getReturnTypeNode(parent);
|
|
2261
|
+
}
|
|
2262
|
+
if (isCallExpression(parent) && isIdentifier(parent.expression)) {
|
|
2263
|
+
const index = (parent.arguments ?? []).indexOf(node);
|
|
2264
|
+
const fn = declaredFunctions.get(parent.expression.text);
|
|
2265
|
+
if (index > -1 && fn)
|
|
2266
|
+
return fn.parameters?.[index]?.type;
|
|
2267
|
+
}
|
|
2268
|
+
if (isPropertyAssignment(parent) &&
|
|
2269
|
+
parent.initializer === node &&
|
|
2270
|
+
(isIdentifier(parent.name) || isStringLiteral(parent.name))) {
|
|
2271
|
+
const objectType = getContextualTypeNode(parent.parent);
|
|
2272
|
+
return objectType ? getMemberTypeNode(objectType, parent.name.text) : undefined;
|
|
2273
|
+
}
|
|
2274
|
+
if (isArrayLiteralExpression(parent)) {
|
|
2275
|
+
const arrayType = resolveTypeNode(getContextualTypeNode(parent));
|
|
2276
|
+
if (isArrayTypeNode(arrayType))
|
|
2277
|
+
return arrayType.elementType;
|
|
2278
|
+
}
|
|
2279
|
+
return undefined;
|
|
2280
|
+
}
|
|
2281
|
+
// Element type of an array literal: from its contextual type (declared
|
|
2282
|
+
// variable/parameter/return type), else inferred from its first typed element
|
|
2283
|
+
function getArrayLiteralElementType(node) {
|
|
2284
|
+
const contextual = resolveTypeNode(getContextualTypeNode(node));
|
|
2285
|
+
if (isArrayTypeNode(contextual))
|
|
2286
|
+
return getType(contextual.elementType);
|
|
2287
|
+
if (contextual?.kind === 'TupleType')
|
|
2288
|
+
return getTupleElementType(contextual);
|
|
2289
|
+
if (isTypeReferenceNode(contextual) &&
|
|
2290
|
+
isIdentifier(contextual.typeName) &&
|
|
2291
|
+
contextual.typeName.text === 'Array' &&
|
|
2292
|
+
contextual.typeArguments?.length === 1) {
|
|
2293
|
+
return getType(contextual.typeArguments[0]);
|
|
2294
|
+
}
|
|
2295
|
+
for (const element of node.elements ?? []) {
|
|
2296
|
+
if (isSpreadElement(element)) {
|
|
2297
|
+
const spreadType = inferExpressionType(element.expression);
|
|
2298
|
+
if (spreadType?.startsWith('[]'))
|
|
2299
|
+
return spreadType.slice(2);
|
|
2300
|
+
if (spreadType?.startsWith('*TnMap[') && extractMapValueType(spreadType) === 'struct{}') {
|
|
2301
|
+
return extractMapKeyType(spreadType);
|
|
2302
|
+
}
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
const elementType = inferExpressionType(element);
|
|
2306
|
+
if (elementType && elementType !== 'nil')
|
|
2307
|
+
return elementType;
|
|
2308
|
+
}
|
|
2309
|
+
return 'interface{}';
|
|
2310
|
+
}
|
|
2311
|
+
// Loop bodies must be blocks in Go; `prefix` is emitted before the body's statements
|
|
2312
|
+
function visitLoopBody(statement, prefix = '') {
|
|
2313
|
+
if (isBlock(statement))
|
|
2314
|
+
return visit(statement, { prefixBlockContent: prefix });
|
|
2315
|
+
return `{\n\t\t${prefix}${visit(statement)}}\n\t`;
|
|
2316
|
+
}
|
|
2317
|
+
// `(x = expr) op rhs` as a loop condition: returns the assignment to hoist
|
|
2318
|
+
function getHoistedConditionAssignment(condition) {
|
|
2319
|
+
if (!isBinaryExpression(condition))
|
|
2320
|
+
return undefined;
|
|
2321
|
+
let left = condition.left;
|
|
2322
|
+
while (isParenthesizedExpression(left))
|
|
2323
|
+
left = left.expression;
|
|
2324
|
+
if (isBinaryExpression(left) && left.operatorToken.kind === 'EqualsToken')
|
|
2325
|
+
return left;
|
|
2326
|
+
return undefined;
|
|
2327
|
+
}
|
|
2328
|
+
// Function type a function literal is expected to have (e.g. from a declared
|
|
2329
|
+
// variable, parameter, or Record value type)
|
|
2330
|
+
function getContextualFunctionType(fn) {
|
|
2331
|
+
if (!isArrowFunction(fn) && !isFunctionExpression(fn))
|
|
2332
|
+
return undefined;
|
|
2333
|
+
const contextual = resolveTypeNode(getContextualTypeNode(fn));
|
|
2334
|
+
return isFunctionTypeNode(contextual) ? contextual : undefined;
|
|
2335
|
+
}
|
|
2336
|
+
function unwrapParentheses(expr) {
|
|
2337
|
+
while (isParenthesizedExpression(expr))
|
|
2338
|
+
expr = expr.expression;
|
|
2339
|
+
return expr;
|
|
2340
|
+
}
|
|
2341
|
+
function getTupleElementType(tuple) {
|
|
2342
|
+
const types = (tuple.elements ?? []).map((t) => getType(t.kind === 'NamedTupleMember' ? t.type : t));
|
|
2343
|
+
return types.length > 0 && types.every((t) => t === types[0]) ? types[0] : 'interface{}';
|
|
2344
|
+
}
|
|
1109
2345
|
function getTypeText(typeNode) {
|
|
1110
2346
|
if (!typeNode)
|
|
1111
2347
|
return ':';
|
|
@@ -1117,6 +2353,14 @@ function getTypeText(typeNode) {
|
|
|
1117
2353
|
function toGoStringLiteral(value) {
|
|
1118
2354
|
return JSON.stringify(value);
|
|
1119
2355
|
}
|
|
2356
|
+
// A spread source as a Go slice; Sets spread their elements (map keys)
|
|
2357
|
+
function visitSpreadSource(expr) {
|
|
2358
|
+
const sourceType = inferExpressionType(expr);
|
|
2359
|
+
if (sourceType?.startsWith('*TnMap[') && extractMapValueType(sourceType) === 'struct{}') {
|
|
2360
|
+
return `${visit(expr)}.Keys()`;
|
|
2361
|
+
}
|
|
2362
|
+
return visit(expr);
|
|
2363
|
+
}
|
|
1120
2364
|
function visitSpreadArrayLiteral(node, elemType) {
|
|
1121
2365
|
const chunks = [];
|
|
1122
2366
|
for (const el of (node.elements ?? [])) {
|
|
@@ -1124,7 +2368,7 @@ function visitSpreadArrayLiteral(node, elemType) {
|
|
|
1124
2368
|
chunks.push({ isSpread: true, items: [el.expression] });
|
|
1125
2369
|
}
|
|
1126
2370
|
else {
|
|
1127
|
-
const last = chunks[chunks.length - 1];
|
|
2371
|
+
const last = chunks.length > 0 ? chunks[chunks.length - 1] : undefined;
|
|
1128
2372
|
if (last && !last.isSpread) {
|
|
1129
2373
|
last.items.push(el);
|
|
1130
2374
|
}
|
|
@@ -1137,7 +2381,7 @@ function visitSpreadArrayLiteral(node, elemType) {
|
|
|
1137
2381
|
let result = `[]${baseType}{}`;
|
|
1138
2382
|
for (const chunk of chunks) {
|
|
1139
2383
|
if (chunk.isSpread) {
|
|
1140
|
-
result = `append(${result}, ${
|
|
2384
|
+
result = `append(${result}, ${visitSpreadSource(chunk.items[0])}...)`;
|
|
1141
2385
|
}
|
|
1142
2386
|
else {
|
|
1143
2387
|
result = `append(${result}, ${chunk.items.map((e) => visit(e)).join(', ')})`;
|
|
@@ -1151,8 +2395,14 @@ function visitTemplateExpression(node) {
|
|
|
1151
2395
|
parts.push(toGoStringLiteral(node.head.text));
|
|
1152
2396
|
}
|
|
1153
2397
|
for (const span of node.templateSpans) {
|
|
1154
|
-
|
|
1155
|
-
|
|
2398
|
+
const spanType = inferExpressionType(span.expression);
|
|
2399
|
+
if (spanType && NULLABLE_PRIMITIVE_TYPES.includes(spanType)) {
|
|
2400
|
+
useHelper('dynamic');
|
|
2401
|
+
parts.push(`TnFormat(${visit(span.expression)})`);
|
|
2402
|
+
}
|
|
2403
|
+
else {
|
|
2404
|
+
parts.push(jsStringOf(visit(span.expression), spanType));
|
|
2405
|
+
}
|
|
1156
2406
|
if (span.literal.text.length > 0) {
|
|
1157
2407
|
parts.push(toGoStringLiteral(span.literal.text));
|
|
1158
2408
|
}
|
|
@@ -1211,6 +2461,9 @@ function prescanVariableDeclarations(block) {
|
|
|
1211
2461
|
function inferFunctionBodyReturnType(node) {
|
|
1212
2462
|
if (node.type)
|
|
1213
2463
|
return getType(node.type);
|
|
2464
|
+
const contextualFn = getContextualFunctionType(node);
|
|
2465
|
+
if (contextualFn?.type)
|
|
2466
|
+
return getType(contextualFn.type);
|
|
1214
2467
|
if (isArrowFunction(node) && !isBlock(node.body)) {
|
|
1215
2468
|
return inferExpressionType(node.body);
|
|
1216
2469
|
}
|
|
@@ -1226,7 +2479,15 @@ function inferFunctionBodyReturnType(node) {
|
|
|
1226
2479
|
return undefined;
|
|
1227
2480
|
}
|
|
1228
2481
|
function inferArrowFunctionGoType(node) {
|
|
1229
|
-
|
|
2482
|
+
// Mirrors getFunctionParametersInfo: trailing defaulted parameters become
|
|
2483
|
+
// a variadic `...interface{}`
|
|
2484
|
+
const parameters = node.parameters ?? [];
|
|
2485
|
+
const firstDefaultIndex = parameters.findIndex((p) => !!p.initializer);
|
|
2486
|
+
const hasTrailingDefaults = firstDefaultIndex > -1 && !parameters.slice(firstDefaultIndex).some((p) => !p.initializer);
|
|
2487
|
+
const paramTypes = (hasTrailingDefaults ? parameters.slice(0, firstDefaultIndex) : parameters).map((p) => getParameterGoType(p));
|
|
2488
|
+
if (hasTrailingDefaults)
|
|
2489
|
+
paramTypes.push('...interface{}');
|
|
2490
|
+
const params = paramTypes.join(', ');
|
|
1230
2491
|
const retType = inferFunctionBodyReturnType(node);
|
|
1231
2492
|
return `func(${params})${retType ? ` ${retType}` : ''}`;
|
|
1232
2493
|
}
|
|
@@ -1236,10 +2497,12 @@ function inferExpressionType(expr) {
|
|
|
1236
2497
|
}
|
|
1237
2498
|
if (isParenthesizedExpression(expr))
|
|
1238
2499
|
return inferExpressionType(expr.expression);
|
|
1239
|
-
if (isNonNullExpression(expr))
|
|
1240
|
-
|
|
2500
|
+
if (isNonNullExpression(expr)) {
|
|
2501
|
+
const innerType = inferExpressionType(expr.expression);
|
|
2502
|
+
return innerType && NULLABLE_PRIMITIVE_TYPES.includes(innerType) ? innerType.slice(1) : innerType;
|
|
2503
|
+
}
|
|
1241
2504
|
if (isAsExpression(expr))
|
|
1242
|
-
return
|
|
2505
|
+
return getAsExpressionType(expr);
|
|
1243
2506
|
if (isTypeAssertionExpression(expr))
|
|
1244
2507
|
return getType(expr.type);
|
|
1245
2508
|
if (isStringLiteral(expr) ||
|
|
@@ -1249,29 +2512,144 @@ function inferExpressionType(expr) {
|
|
|
1249
2512
|
}
|
|
1250
2513
|
if (isNumericLiteral(expr))
|
|
1251
2514
|
return 'float64';
|
|
2515
|
+
if (expr.kind === 'TypeOfExpression')
|
|
2516
|
+
return 'string';
|
|
2517
|
+
if (isRegularExpressionLiteral(expr))
|
|
2518
|
+
return '*regexp.Regexp';
|
|
2519
|
+
if (isNewExpression(expr) && isIdentifier(expr.expression) && expr.expression.text === 'RegExp') {
|
|
2520
|
+
return '*regexp.Regexp';
|
|
2521
|
+
}
|
|
1252
2522
|
if (expr.kind === 'TrueKeyword' || expr.kind === 'FalseKeyword')
|
|
1253
2523
|
return 'bool';
|
|
1254
2524
|
if (expr.kind === 'NullKeyword')
|
|
1255
2525
|
return 'nil';
|
|
1256
|
-
if (isIdentifier(expr))
|
|
1257
|
-
return
|
|
2526
|
+
if (isIdentifier(expr) && expr.text === 'undefined')
|
|
2527
|
+
return 'nil';
|
|
2528
|
+
if (isIdentifier(expr)) {
|
|
2529
|
+
const varType = variableGoTypes.get(expr.text);
|
|
2530
|
+
return narrowedVariables.has(expr.text) && varType?.startsWith('*') ? varType.slice(1) : varType;
|
|
2531
|
+
}
|
|
1258
2532
|
if (isArrayLiteralExpression(expr)) {
|
|
1259
|
-
|
|
1260
|
-
if (elements.length === 0)
|
|
1261
|
-
return '[]interface{}';
|
|
1262
|
-
const firstElementType = inferExpressionType(elements[0]) ?? 'interface{}';
|
|
1263
|
-
return `[]${firstElementType}`;
|
|
2533
|
+
return `[]${getArrayLiteralElementType(expr)}`;
|
|
1264
2534
|
}
|
|
2535
|
+
if (isObjectLiteralExpression(expr))
|
|
2536
|
+
return getObjectLiteralGoType(expr);
|
|
1265
2537
|
if (isNewExpression(expr) && isIdentifier(expr.expression)) {
|
|
1266
2538
|
const ctorName = expr.expression.text;
|
|
1267
|
-
|
|
1268
|
-
|
|
2539
|
+
const typeArguments = getCollectionTypeArguments(expr);
|
|
2540
|
+
if (ctorName === 'Map' && typeArguments.length === 2) {
|
|
2541
|
+
return mapGoType(getType(typeArguments[0]), getType(typeArguments[1]));
|
|
1269
2542
|
}
|
|
1270
|
-
if (ctorName === 'Set' &&
|
|
1271
|
-
return
|
|
2543
|
+
if (ctorName === 'Set' && typeArguments.length === 1) {
|
|
2544
|
+
return mapGoType(getType(typeArguments[0]), 'struct{}');
|
|
1272
2545
|
}
|
|
2546
|
+
if (ctorName === 'Set' && expr.arguments?.length && isArrayLiteralExpression(expr.arguments[0])) {
|
|
2547
|
+
return mapGoType(getArrayLiteralElementType(expr.arguments[0]), 'struct{}');
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
if (isElementAccessExpression(expr)) {
|
|
2551
|
+
const objectType = inferExpressionType(expr.expression);
|
|
2552
|
+
if (objectType === 'string')
|
|
2553
|
+
return 'string';
|
|
2554
|
+
if (objectType?.startsWith('[]'))
|
|
2555
|
+
return objectType.slice(2);
|
|
2556
|
+
if (objectType?.startsWith('*TnMap['))
|
|
2557
|
+
return extractMapValueType(objectType);
|
|
2558
|
+
const declared = getExpressionTypeNode(expr);
|
|
2559
|
+
if (declared)
|
|
2560
|
+
return getType(declared);
|
|
2561
|
+
}
|
|
2562
|
+
if (isPropertyAccessExpression(expr) &&
|
|
2563
|
+
isIdentifier(expr.expression) &&
|
|
2564
|
+
expr.expression.text === 'process' &&
|
|
2565
|
+
expr.name.text === 'argv') {
|
|
2566
|
+
return '[]string';
|
|
2567
|
+
}
|
|
2568
|
+
if (isIdentifier(expr) && (expr.text === 'NaN' || expr.text === 'Infinity'))
|
|
2569
|
+
return 'float64';
|
|
2570
|
+
if (isPropertyAccessExpression(expr) && isIdentifier(expr.expression) && expr.expression.text === 'Number') {
|
|
2571
|
+
return 'float64';
|
|
2572
|
+
}
|
|
2573
|
+
// process.env.X (also through casts: (process.env as any).X)
|
|
2574
|
+
if (isPropertyAccessExpression(expr) && isProcessEnv(expr.expression))
|
|
2575
|
+
return 'string';
|
|
2576
|
+
if ((isPrefixUnaryExpression(expr) || isPostfixUnaryExpression(expr)) &&
|
|
2577
|
+
['MinusToken', 'PlusToken', 'TildeToken', 'PlusPlusToken', 'MinusMinusToken'].includes(expr.operator)) {
|
|
2578
|
+
return 'float64';
|
|
2579
|
+
}
|
|
2580
|
+
if (isCallExpression(expr)) {
|
|
2581
|
+
const callee = isIdentifier(expr.expression)
|
|
2582
|
+
? expr.expression.text
|
|
2583
|
+
: isPropertyAccessExpression(expr.expression) && isIdentifier(expr.expression.expression)
|
|
2584
|
+
? `${expr.expression.expression.text}.${expr.expression.name.text}`
|
|
2585
|
+
: undefined;
|
|
2586
|
+
const nodeResultType = callee ? nodeCallResultTypes.get(callee) : undefined;
|
|
2587
|
+
if (callee && BUILTIN_CALL_TYPES[callee])
|
|
2588
|
+
return BUILTIN_CALL_TYPES[callee];
|
|
2589
|
+
if (callee?.startsWith('Math.'))
|
|
2590
|
+
return 'float64';
|
|
2591
|
+
if (nodeResultType)
|
|
2592
|
+
return nodeResultType;
|
|
2593
|
+
}
|
|
2594
|
+
// Object.keys(map) / Object.values(map)
|
|
2595
|
+
if (isCallExpression(expr) &&
|
|
2596
|
+
isPropertyAccessExpression(expr.expression) &&
|
|
2597
|
+
isIdentifier(expr.expression.expression) &&
|
|
2598
|
+
expr.expression.expression.text === 'Object') {
|
|
2599
|
+
const mapType = inferExpressionType(expr.arguments?.[0]);
|
|
2600
|
+
if (mapType?.startsWith('*TnMap[')) {
|
|
2601
|
+
if (expr.expression.name.text === 'keys')
|
|
2602
|
+
return `[]${extractMapKeyType(mapType)}`;
|
|
2603
|
+
if (expr.expression.name.text === 'values')
|
|
2604
|
+
return `[]${extractMapValueType(mapType)}`;
|
|
2605
|
+
if (expr.expression.name.text === 'entries')
|
|
2606
|
+
return '[][]interface{}';
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (isCallExpression(expr) && isIdentifier(expr.expression)) {
|
|
2610
|
+
const builtinType = BUILTIN_FUNCTION_TYPES[expr.expression.text];
|
|
2611
|
+
if (builtinType && !declaredFunctions.has(expr.expression.text))
|
|
2612
|
+
return builtinType;
|
|
2613
|
+
}
|
|
2614
|
+
if (isPropertyAccessExpression(expr) && expr.name.text === 'length' && isDynamicValue(expr.expression)) {
|
|
2615
|
+
return 'float64';
|
|
2616
|
+
}
|
|
2617
|
+
if ((isPropertyAccessExpression(expr) || isElementAccessExpression(expr)) && isDynamicValue(expr.expression)) {
|
|
2618
|
+
return 'interface{}';
|
|
2619
|
+
}
|
|
2620
|
+
if (isCallExpression(expr) && isObjectKeysOfDynamic(expr))
|
|
2621
|
+
return '[]string';
|
|
2622
|
+
if (isCallExpression(expr) &&
|
|
2623
|
+
isPropertyAccessExpression(expr.expression) &&
|
|
2624
|
+
isIdentifier(expr.expression.expression) &&
|
|
2625
|
+
expr.expression.expression.text === 'JSON' &&
|
|
2626
|
+
expr.expression.name.text === 'parse') {
|
|
2627
|
+
return 'interface{}';
|
|
2628
|
+
}
|
|
2629
|
+
if (isCallExpression(expr) &&
|
|
2630
|
+
isPropertyAccessExpression(expr.expression) &&
|
|
2631
|
+
isDynamicValue(expr.expression.expression)) {
|
|
2632
|
+
const method = expr.expression.name.text;
|
|
2633
|
+
const castCall = castDynamicReceiverCall(expr);
|
|
2634
|
+
if (castCall)
|
|
2635
|
+
return inferExpressionType(castCall);
|
|
2636
|
+
if (method === 'includes')
|
|
2637
|
+
return 'bool';
|
|
2638
|
+
if (method === 'indexOf')
|
|
2639
|
+
return 'float64';
|
|
2640
|
+
if (method === 'slice' || method === 'concat')
|
|
2641
|
+
return 'interface{}';
|
|
1273
2642
|
}
|
|
1274
2643
|
if (isPropertyAccessExpression(expr)) {
|
|
2644
|
+
const narrowingKey = getNarrowingKey(expr);
|
|
2645
|
+
if (narrowingKey && narrowedVariables.has(narrowingKey)) {
|
|
2646
|
+
// Narrowed: the declared (pointer) type without its pointer
|
|
2647
|
+
narrowedVariables.delete(narrowingKey);
|
|
2648
|
+
const nullableType = inferExpressionType(expr);
|
|
2649
|
+
narrowedVariables.add(narrowingKey);
|
|
2650
|
+
if (nullableType?.startsWith('*'))
|
|
2651
|
+
return nullableType.slice(1);
|
|
2652
|
+
}
|
|
1275
2653
|
if (isIdentifier(expr.expression) && enumNames.has(expr.expression.text)) {
|
|
1276
2654
|
const enumType = getSafeName(expr.expression.text);
|
|
1277
2655
|
return enumType;
|
|
@@ -1286,7 +2664,9 @@ function inferExpressionType(expr) {
|
|
|
1286
2664
|
: undefined;
|
|
1287
2665
|
if (hasQuestionDot(expr)) {
|
|
1288
2666
|
if (leftType && leftType.startsWith('*')) {
|
|
1289
|
-
const memberType = resolvedPropertyType ??
|
|
2667
|
+
const memberType = resolvedPropertyType ??
|
|
2668
|
+
getStructFieldGoType(leftType.slice(1), expr.name.text) ??
|
|
2669
|
+
'interface{}';
|
|
1290
2670
|
return makeNullableType(memberType);
|
|
1291
2671
|
}
|
|
1292
2672
|
return 'interface{}';
|
|
@@ -1294,6 +2674,9 @@ function inferExpressionType(expr) {
|
|
|
1294
2674
|
if (resolvedPropertyType) {
|
|
1295
2675
|
return resolvedPropertyType;
|
|
1296
2676
|
}
|
|
2677
|
+
const structFieldType = leftType ? getStructFieldGoType(leftType.replace(/^\*/, ''), expr.name.text) : undefined;
|
|
2678
|
+
if (structFieldType)
|
|
2679
|
+
return structFieldType;
|
|
1297
2680
|
if (isIdentifier(expr.expression)) {
|
|
1298
2681
|
const className = variableClassNames.get(expr.expression.text);
|
|
1299
2682
|
const memberType = className
|
|
@@ -1303,14 +2686,46 @@ function inferExpressionType(expr) {
|
|
|
1303
2686
|
return memberType;
|
|
1304
2687
|
}
|
|
1305
2688
|
}
|
|
2689
|
+
if (isCallExpression(expr) && isIdentifier(expr.expression)) {
|
|
2690
|
+
const fn = declaredFunctions.get(expr.expression.text);
|
|
2691
|
+
if (fn?.type)
|
|
2692
|
+
return getType(fn.type);
|
|
2693
|
+
const fnValueType = variableGoTypes.get(expr.expression.text);
|
|
2694
|
+
if (fnValueType?.startsWith('func('))
|
|
2695
|
+
return goFuncReturnType(fnValueType);
|
|
2696
|
+
}
|
|
1306
2697
|
if (isCallExpression(expr) && isPropertyAccessExpression(expr.expression)) {
|
|
1307
2698
|
const methodName = expr.expression.name.text;
|
|
1308
2699
|
const ownerType = inferExpressionType(expr.expression.expression);
|
|
1309
|
-
if (ownerType &&
|
|
2700
|
+
if (ownerType &&
|
|
2701
|
+
NULLABLE_PRIMITIVE_TYPES.includes(ownerType) &&
|
|
2702
|
+
(hasQuestionDot(expr) ||
|
|
2703
|
+
hasQuestionDot(expr.expression) ||
|
|
2704
|
+
isOptionalChain(expr.expression.expression))) {
|
|
2705
|
+
const tmp = getTempName('optt');
|
|
2706
|
+
const call = makeNarrowedReceiverCall(expr, tmp, ownerType);
|
|
2707
|
+
const resultType = withNarrowingType([tmp], () => inferExpressionType(call));
|
|
2708
|
+
variableGoTypes.delete(tmp);
|
|
2709
|
+
return resultType ? makeNullableType(resultType) : undefined;
|
|
2710
|
+
}
|
|
2711
|
+
if (ownerType === 'string') {
|
|
2712
|
+
const stringMethodType = STRING_METHOD_RETURN_TYPES[methodName];
|
|
2713
|
+
if (stringMethodType)
|
|
2714
|
+
return stringMethodType;
|
|
2715
|
+
}
|
|
2716
|
+
if (ownerType === '*regexp.Regexp') {
|
|
2717
|
+
if (methodName === 'exec')
|
|
2718
|
+
return '[]string';
|
|
2719
|
+
if (methodName === 'test')
|
|
2720
|
+
return 'bool';
|
|
2721
|
+
}
|
|
2722
|
+
if (ownerType && ownerType.startsWith('*TnMap[')) {
|
|
1310
2723
|
if (methodName === 'has')
|
|
1311
2724
|
return 'bool';
|
|
1312
|
-
if (methodName === 'get')
|
|
1313
|
-
|
|
2725
|
+
if (methodName === 'get') {
|
|
2726
|
+
const valueType = extractMapValueType(ownerType);
|
|
2727
|
+
return isStructGoType(valueType) ? `*${valueType}` : valueType;
|
|
2728
|
+
}
|
|
1314
2729
|
}
|
|
1315
2730
|
if (isArrayLikeGoType(ownerType)) {
|
|
1316
2731
|
const elementType = getArrayElementTypeFromGoType(ownerType);
|
|
@@ -1321,10 +2736,17 @@ function inferExpressionType(expr) {
|
|
|
1321
2736
|
: elementType;
|
|
1322
2737
|
return `[]${mappedType}`;
|
|
1323
2738
|
}
|
|
1324
|
-
if (
|
|
2739
|
+
if (['filter', 'slice', 'concat', 'reverse', 'sort'].includes(methodName)) {
|
|
1325
2740
|
return `[]${elementType}`;
|
|
1326
|
-
|
|
2741
|
+
}
|
|
2742
|
+
if (['some', 'every', 'includes'].includes(methodName))
|
|
1327
2743
|
return 'bool';
|
|
2744
|
+
if (['indexOf', 'lastIndexOf', 'findIndex'].includes(methodName))
|
|
2745
|
+
return 'float64';
|
|
2746
|
+
if (['pop', 'shift', 'at'].includes(methodName))
|
|
2747
|
+
return elementType;
|
|
2748
|
+
if (methodName === 'flat' && elementType.startsWith('[]'))
|
|
2749
|
+
return elementType;
|
|
1328
2750
|
if (methodName === 'find')
|
|
1329
2751
|
return elementType;
|
|
1330
2752
|
if (methodName === 'join')
|
|
@@ -1356,55 +2778,352 @@ function inferExpressionType(expr) {
|
|
|
1356
2778
|
if (isConditionalExpression(expr)) {
|
|
1357
2779
|
const whenTrueType = inferExpressionType(expr.whenTrue);
|
|
1358
2780
|
const whenFalseType = inferExpressionType(expr.whenFalse);
|
|
2781
|
+
// `c ? value : undefined` is nullable
|
|
2782
|
+
if (whenTrueType === 'nil' && whenFalseType)
|
|
2783
|
+
return makeNullableType(whenFalseType);
|
|
2784
|
+
if (whenFalseType === 'nil' && whenTrueType)
|
|
2785
|
+
return makeNullableType(whenTrueType);
|
|
1359
2786
|
if (whenTrueType && whenTrueType === whenFalseType)
|
|
1360
2787
|
return whenTrueType;
|
|
1361
|
-
|
|
2788
|
+
if (whenTrueType && whenFalseType === `*${whenTrueType}`)
|
|
2789
|
+
return whenFalseType;
|
|
2790
|
+
if (whenFalseType && whenTrueType === `*${whenFalseType}`)
|
|
2791
|
+
return whenTrueType;
|
|
2792
|
+
const branchType = whenTrueType ?? whenFalseType;
|
|
2793
|
+
return branchType === 'nil' ? 'interface{}' : branchType;
|
|
1362
2794
|
}
|
|
1363
2795
|
if (isBinaryExpression(expr) &&
|
|
1364
2796
|
expr.operatorToken.kind === 'QuestionQuestionToken') {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
2797
|
+
return getNullishResultType(expr);
|
|
2798
|
+
}
|
|
2799
|
+
if (isPrefixUnaryExpression(expr) && expr.operator === 'ExclamationToken')
|
|
2800
|
+
return 'bool';
|
|
2801
|
+
if (isBinaryExpression(expr)) {
|
|
2802
|
+
const kind = expr.operatorToken.kind;
|
|
2803
|
+
if (COMPARISON_OPERATORS.has(kind) || kind === 'InKeyword' || kind === 'InstanceOfKeyword') {
|
|
2804
|
+
return 'bool';
|
|
2805
|
+
}
|
|
2806
|
+
if (isLogicalOperator(expr.operatorToken)) {
|
|
2807
|
+
return isLogicalValueExpression(expr) ? getLogicalValueType(expr) : 'bool';
|
|
2808
|
+
}
|
|
2809
|
+
if ([
|
|
2810
|
+
'MinusToken',
|
|
2811
|
+
'AsteriskToken',
|
|
2812
|
+
'SlashToken',
|
|
2813
|
+
'PercentToken',
|
|
2814
|
+
'AsteriskAsteriskToken',
|
|
2815
|
+
'AmpersandToken',
|
|
2816
|
+
'BarToken',
|
|
2817
|
+
'CaretToken',
|
|
2818
|
+
'LessThanLessThanToken',
|
|
2819
|
+
'GreaterThanGreaterThanToken',
|
|
2820
|
+
'GreaterThanGreaterThanGreaterThanToken'
|
|
2821
|
+
].includes(kind)) {
|
|
2822
|
+
return 'float64';
|
|
2823
|
+
}
|
|
2824
|
+
if (kind === 'PlusToken') {
|
|
2825
|
+
const isString = [expr.left, expr.right].some((side) => inferExpressionType(side) === 'string');
|
|
2826
|
+
return isString ? 'string' : 'float64';
|
|
2827
|
+
}
|
|
2828
|
+
if (['LessThanToken', 'GreaterThanToken', 'LessThanEqualsToken', 'GreaterThanEqualsToken'].includes(kind)) {
|
|
2829
|
+
return 'bool';
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
return undefined;
|
|
2833
|
+
}
|
|
2834
|
+
const BUILTIN_CALL_TYPES = {
|
|
2835
|
+
'Number.isNaN': 'bool',
|
|
2836
|
+
'Number.isFinite': 'bool',
|
|
2837
|
+
'Number.isInteger': 'bool',
|
|
2838
|
+
isNaN: 'bool'
|
|
2839
|
+
};
|
|
2840
|
+
const BUILTIN_FUNCTION_TYPES = {
|
|
2841
|
+
parseFloat: 'float64',
|
|
2842
|
+
parseInt: 'float64',
|
|
2843
|
+
Number: 'float64',
|
|
2844
|
+
String: 'string'
|
|
2845
|
+
};
|
|
2846
|
+
const STRING_METHOD_RETURN_TYPES = {
|
|
2847
|
+
trim: 'string',
|
|
2848
|
+
trimStart: 'string',
|
|
2849
|
+
trimEnd: 'string',
|
|
2850
|
+
toUpperCase: 'string',
|
|
2851
|
+
toLowerCase: 'string',
|
|
2852
|
+
slice: 'string',
|
|
2853
|
+
substring: 'string',
|
|
2854
|
+
replace: 'string',
|
|
2855
|
+
replaceAll: 'string',
|
|
2856
|
+
padStart: 'string',
|
|
2857
|
+
padEnd: 'string',
|
|
2858
|
+
repeat: 'string',
|
|
2859
|
+
charAt: 'string',
|
|
2860
|
+
concat: 'string',
|
|
2861
|
+
at: 'string',
|
|
2862
|
+
split: '[]string',
|
|
2863
|
+
match: '[]string',
|
|
2864
|
+
matchAll: '[][]string',
|
|
2865
|
+
includes: 'bool',
|
|
2866
|
+
startsWith: 'bool',
|
|
2867
|
+
endsWith: 'bool',
|
|
2868
|
+
indexOf: 'float64',
|
|
2869
|
+
lastIndexOf: 'float64',
|
|
2870
|
+
search: 'float64',
|
|
2871
|
+
charCodeAt: 'float64'
|
|
2872
|
+
};
|
|
2873
|
+
// Return type of a Go func type string: `func(a T) R` → `R`
|
|
2874
|
+
function goFuncReturnType(funcType) {
|
|
2875
|
+
let depth = 0;
|
|
2876
|
+
for (let i = 4; i < funcType.length; i++) {
|
|
2877
|
+
const ch = funcType[i];
|
|
2878
|
+
if (ch === '(')
|
|
2879
|
+
depth++;
|
|
2880
|
+
else if (ch === ')') {
|
|
2881
|
+
depth--;
|
|
2882
|
+
if (depth === 0)
|
|
2883
|
+
return funcType.slice(i + 1).trim() || undefined;
|
|
1369
2884
|
}
|
|
1370
|
-
return rightType ?? leftType;
|
|
1371
2885
|
}
|
|
1372
2886
|
return undefined;
|
|
1373
2887
|
}
|
|
2888
|
+
const COMPARISON_OPERATORS = new Set([
|
|
2889
|
+
'EqualsEqualsEqualsToken',
|
|
2890
|
+
'ExclamationEqualsEqualsToken',
|
|
2891
|
+
'EqualsEqualsToken',
|
|
2892
|
+
'ExclamationEqualsToken',
|
|
2893
|
+
'LessThanToken',
|
|
2894
|
+
'LessThanEqualsToken',
|
|
2895
|
+
'GreaterThanToken',
|
|
2896
|
+
'GreaterThanEqualsToken'
|
|
2897
|
+
]);
|
|
2898
|
+
function isLogicalOperator(token) {
|
|
2899
|
+
return token?.kind === 'AmpersandAmpersandToken' || token?.kind === 'BarBarToken';
|
|
2900
|
+
}
|
|
2901
|
+
// JS truthiness of an expression as a Go bool, by its Go type
|
|
2902
|
+
function toGoCondition(expr) {
|
|
2903
|
+
if (isParenthesizedExpression(expr))
|
|
2904
|
+
return `(${toGoCondition(expr.expression)})`;
|
|
2905
|
+
if (isPrefixUnaryExpression(expr) && expr.operator === 'ExclamationToken') {
|
|
2906
|
+
return `!${wrapCondition(toGoCondition(expr.operand))}`;
|
|
2907
|
+
}
|
|
2908
|
+
if (isBinaryExpression(expr) && isLogicalOperator(expr.operatorToken)) {
|
|
2909
|
+
const isAnd = expr.operatorToken.kind === 'AmpersandAmpersandToken';
|
|
2910
|
+
const right = withNarrowing(getNarrowedNames(expr.left, isAnd), () => toGoCondition(expr.right));
|
|
2911
|
+
return `${toGoCondition(expr.left)} ${isAnd ? '&&' : '||'} ${right}`;
|
|
2912
|
+
}
|
|
2913
|
+
return truthinessCheck(visit(expr, { inline: true }), inferExpressionType(expr));
|
|
2914
|
+
}
|
|
2915
|
+
function truthinessCheck(code, goType) {
|
|
2916
|
+
if (!goType || goType === 'bool' || goType === ':')
|
|
2917
|
+
return code;
|
|
2918
|
+
if (goType === 'interface{}') {
|
|
2919
|
+
useHelper('dynamic');
|
|
2920
|
+
return `TnTruthy(${code})`;
|
|
2921
|
+
}
|
|
2922
|
+
if (NULLABLE_PRIMITIVE_TYPES.includes(goType) && !/^[\w.()*]+$/.test(code)) {
|
|
2923
|
+
return `func() bool { __t := ${code}; return ${truthinessCheck('__t', goType)} }()`;
|
|
2924
|
+
}
|
|
2925
|
+
// structs (value types) are always truthy
|
|
2926
|
+
if (isStructGoType(goType))
|
|
2927
|
+
return 'true';
|
|
2928
|
+
if (goType === 'string')
|
|
2929
|
+
return `${code} != ""`;
|
|
2930
|
+
if (goType === 'float64')
|
|
2931
|
+
return `${code} != 0`;
|
|
2932
|
+
if (goType === '*bool')
|
|
2933
|
+
return `(${code} != nil && *${code})`;
|
|
2934
|
+
if (goType === '*string')
|
|
2935
|
+
return `(${code} != nil && *${code} != "")`;
|
|
2936
|
+
if (goType === '*float64')
|
|
2937
|
+
return `(${code} != nil && *${code} != 0)`;
|
|
2938
|
+
if (goType.startsWith('*') ||
|
|
2939
|
+
goType.startsWith('[]') ||
|
|
2940
|
+
goType.startsWith('map[') ||
|
|
2941
|
+
goType.startsWith('func') ||
|
|
2942
|
+
goType.startsWith('chan ') ||
|
|
2943
|
+
goType === 'interface{}') {
|
|
2944
|
+
return `${code} != nil`;
|
|
2945
|
+
}
|
|
2946
|
+
return code;
|
|
2947
|
+
}
|
|
2948
|
+
// Parenthesizes a condition unless it is already atomic
|
|
2949
|
+
function wrapCondition(condition) {
|
|
2950
|
+
return /^[\w.]+$/.test(condition) || /^\(.*\)$/.test(condition) ? condition : `(${condition})`;
|
|
2951
|
+
}
|
|
2952
|
+
// `a || b` with non-bool operands is a value (JS returns an operand), not a bool
|
|
2953
|
+
function isLogicalValueExpression(expr) {
|
|
2954
|
+
if (expr.operatorToken.kind !== 'BarBarToken')
|
|
2955
|
+
return false;
|
|
2956
|
+
const leftType = inferExpressionType(expr.left);
|
|
2957
|
+
const rightType = inferExpressionType(expr.right);
|
|
2958
|
+
return !!leftType && leftType !== 'bool' && !!rightType && rightType !== 'bool';
|
|
2959
|
+
}
|
|
2960
|
+
function getLogicalValueType(expr) {
|
|
2961
|
+
const leftType = inferExpressionType(expr.left);
|
|
2962
|
+
const rightType = inferExpressionType(expr.right);
|
|
2963
|
+
if (rightType === 'nil')
|
|
2964
|
+
return makeNullableType(leftType);
|
|
2965
|
+
if (leftType.startsWith('*') && rightType === leftType.slice(1))
|
|
2966
|
+
return rightType;
|
|
2967
|
+
return leftType;
|
|
2968
|
+
}
|
|
2969
|
+
// && / || : boolean form when any operand is non-bool (truthiness), value form for
|
|
2970
|
+
// `a || fallback`; undefined keeps the plain Go operator (both operands bool/unknown)
|
|
2971
|
+
function visitLogicalExpression(expr) {
|
|
2972
|
+
const leftType = inferExpressionType(expr.left);
|
|
2973
|
+
const rightType = inferExpressionType(expr.right);
|
|
2974
|
+
const isBoolOrUnknown = (t) => !t || t === 'bool' || t === ':';
|
|
2975
|
+
if (isBoolOrUnknown(leftType) && isBoolOrUnknown(rightType))
|
|
2976
|
+
return undefined;
|
|
2977
|
+
if (!isLogicalValueExpression(expr))
|
|
2978
|
+
return toGoCondition(expr);
|
|
2979
|
+
// a || b → func() T { if truthy(a) { return a }; return b }()
|
|
2980
|
+
const resultType = getLogicalValueType(expr);
|
|
2981
|
+
const tmp = getTempName('or');
|
|
2982
|
+
let leftValue = tmp;
|
|
2983
|
+
if (leftType.startsWith('*') && resultType === leftType.slice(1))
|
|
2984
|
+
leftValue = `*${tmp}`;
|
|
2985
|
+
else if (resultType === `*${leftType}`)
|
|
2986
|
+
leftValue = `&${tmp}`;
|
|
2987
|
+
return `func() ${resultType} { ${tmp} := ${visit(expr.left)}; if ${truthinessCheck(tmp, leftType)} { return ${leftValue} }; return ${toGoValueOfType(expr.right, resultType)} }()`;
|
|
2988
|
+
}
|
|
1374
2989
|
function makeNullableType(typeName) {
|
|
1375
2990
|
if (!typeName || typeName === 'interface{}' || typeName.startsWith('*'))
|
|
1376
2991
|
return typeName || 'interface{}';
|
|
1377
2992
|
if (['string', 'float64', 'bool'].includes(typeName))
|
|
1378
2993
|
return `*${typeName}`;
|
|
2994
|
+
if (isStructGoType(typeName))
|
|
2995
|
+
return `*${typeName}`;
|
|
1379
2996
|
return typeName;
|
|
1380
2997
|
}
|
|
2998
|
+
// Go struct types (value types that cannot be nil): anonymous structs and
|
|
2999
|
+
// property-only interfaces / object type aliases
|
|
3000
|
+
// Object types that compile to Go structs: property-only interfaces and
|
|
3001
|
+
// object type aliases (values of these types are *Name, like JS references)
|
|
3002
|
+
function isObjectTypeName(name) {
|
|
3003
|
+
const alias = declaredTypeAliases.get(name) ?? typeAliases.get(name);
|
|
3004
|
+
if (alias)
|
|
3005
|
+
return alias.kind === 'TypeLiteral';
|
|
3006
|
+
const iface = declaredInterfaces.get(name);
|
|
3007
|
+
if (iface) {
|
|
3008
|
+
const members = iface.members ?? [];
|
|
3009
|
+
return members.some((m) => isPropertySignature(m)) && !members.some((m) => isMethodSignature(m));
|
|
3010
|
+
}
|
|
3011
|
+
return interfacePropertyTypes.has(name);
|
|
3012
|
+
}
|
|
3013
|
+
function isStructGoType(goType) {
|
|
3014
|
+
return goType.startsWith('struct{') || interfacePropertyTypes.has(goType);
|
|
3015
|
+
}
|
|
1381
3016
|
function visitConditionalExpression(node) {
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
3017
|
+
const resultType = inferExpectedTypeFromContext(node) || inferExpressionType(node) || 'interface{}';
|
|
3018
|
+
const whenTrue = withNarrowing(getNarrowedNames(node.condition, true), () => toGoValueOfType(node.whenTrue, resultType));
|
|
3019
|
+
const whenFalse = withNarrowing(getNarrowedNames(node.condition, false), () => toGoValueOfType(node.whenFalse, resultType));
|
|
3020
|
+
return `func() ${resultType} { if ${toGoCondition(node.condition)} { return ${whenTrue} }; return ${whenFalse} }()`;
|
|
3021
|
+
}
|
|
3022
|
+
// Type of `a ?? b`: NonNullable<A> | B — nullable only when b can be null too
|
|
3023
|
+
function getNullishResultType(expr) {
|
|
3024
|
+
const leftType = inferExpressionType(expr.left);
|
|
3025
|
+
const rightType = inferExpressionType(expr.right);
|
|
3026
|
+
const leftValueType = leftType && NULLABLE_PRIMITIVE_TYPES.includes(leftType) ? leftType.slice(1) : leftType;
|
|
3027
|
+
if (!rightType || rightType === 'nil')
|
|
3028
|
+
return leftType;
|
|
3029
|
+
if (rightType === leftValueType)
|
|
3030
|
+
return rightType;
|
|
3031
|
+
if (leftValueType && rightType === `*${leftValueType}`)
|
|
3032
|
+
return rightType;
|
|
3033
|
+
if (leftValueType === 'interface{}')
|
|
3034
|
+
return 'interface{}';
|
|
3035
|
+
if (!leftValueType)
|
|
3036
|
+
return rightType;
|
|
3037
|
+
return leftValueType;
|
|
3038
|
+
}
|
|
3039
|
+
// Inside a ?? chain an operand keeps its own type; otherwise the context decides
|
|
3040
|
+
function getNullishEmitType(node) {
|
|
3041
|
+
const parent = node.parent;
|
|
3042
|
+
const inChain = isBinaryExpression(parent) && parent.operatorToken.kind === 'QuestionQuestionToken';
|
|
3043
|
+
return (inChain ? undefined : inferExpectedTypeFromContext(node)) || getNullishResultType(node);
|
|
1393
3044
|
}
|
|
3045
|
+
// a ?? b ?? c: operands are tried in order; map lookups (comma-ok) and nil-able
|
|
3046
|
+
// values may be missing, anything else is always defined and ends the chain
|
|
1394
3047
|
function visitNullishCoalescingExpression(node) {
|
|
1395
|
-
const
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
3048
|
+
const operands = flattenNullishChain(node);
|
|
3049
|
+
const resultType = getNullishEmitType(node) || 'interface{}';
|
|
3050
|
+
const steps = [];
|
|
3051
|
+
for (let i = 0; i < operands.length; i++) {
|
|
3052
|
+
const operand = operands[i];
|
|
3053
|
+
const isLast = i === operands.length - 1;
|
|
1401
3054
|
const tmp = getTempName('nullish');
|
|
1402
|
-
const
|
|
1403
|
-
const
|
|
1404
|
-
const
|
|
1405
|
-
|
|
3055
|
+
const lookup = isLast ? undefined : getMapLookup(operand);
|
|
3056
|
+
const operandType = inferExpressionType(operand);
|
|
3057
|
+
const arrayType = !isLast && isElementAccessExpression(operand) ? inferExpressionType(operand.expression) : undefined;
|
|
3058
|
+
if (arrayType?.startsWith('[]')) {
|
|
3059
|
+
// arr[i] ?? x: out of range is undefined in JS
|
|
3060
|
+
const array = getTempName('arr');
|
|
3061
|
+
const index = getTempName('idx');
|
|
3062
|
+
steps.push(`if ${array}, ${index} := ${visit(operand.expression)}, int(${visit(operand.argumentExpression)}); ${index} >= 0 && ${index} < len(${array}) { return ${convertGoValue(`${array}[${index}]`, arrayType.slice(2), resultType)} }`);
|
|
3063
|
+
}
|
|
3064
|
+
else if (lookup) {
|
|
3065
|
+
const valueType = extractMapValueType(inferExpressionType(lookup.mapNode) ?? '');
|
|
3066
|
+
steps.push(`if ${tmp}, ok := ${lookup.map}.Lookup(${lookup.key}); ok { return ${convertGoValue(tmp, valueType, resultType)} }`);
|
|
3067
|
+
}
|
|
3068
|
+
else if (!isLast && operandType && isNilableGoType(operandType)) {
|
|
3069
|
+
steps.push(`if ${tmp} := ${visit(operand)}; ${tmp} != nil { return ${convertGoValue(tmp, operandType, resultType)} }`);
|
|
3070
|
+
}
|
|
3071
|
+
else {
|
|
3072
|
+
steps.push(`return ${toGoValueOfType(operand, resultType)}`);
|
|
3073
|
+
break;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
return `func() ${resultType} { ${steps.join('; ')} }()`;
|
|
3077
|
+
}
|
|
3078
|
+
function flattenNullishChain(node) {
|
|
3079
|
+
if (isBinaryExpression(node) && node.operatorToken.kind === 'QuestionQuestionToken') {
|
|
3080
|
+
return [...flattenNullishChain(node.left), node.right];
|
|
3081
|
+
}
|
|
3082
|
+
if (isParenthesizedExpression(node))
|
|
3083
|
+
return flattenNullishChain(node.expression);
|
|
3084
|
+
return [node];
|
|
3085
|
+
}
|
|
3086
|
+
// Converts a Go variable between T and *T as the target type requires
|
|
3087
|
+
function convertGoValue(variable, fromType, toType) {
|
|
3088
|
+
if (toType === `*${fromType}`)
|
|
3089
|
+
return `&${variable}`;
|
|
3090
|
+
if (fromType === `*${toType}`)
|
|
3091
|
+
return `*${variable}`;
|
|
3092
|
+
// dynamic (any) values hold the concrete type at runtime
|
|
3093
|
+
if (fromType === 'interface{}' && toType !== 'interface{}')
|
|
3094
|
+
return `${variable}.(${toType})`;
|
|
3095
|
+
return variable;
|
|
3096
|
+
}
|
|
3097
|
+
// Map lookups (m.get(k) on a Map, m[k] on a Record): Go code of the map and key
|
|
3098
|
+
function getMapLookup(expr) {
|
|
3099
|
+
if (isCallExpression(expr) &&
|
|
3100
|
+
isPropertyAccessExpression(expr.expression) &&
|
|
3101
|
+
expr.expression.name.text === 'get' &&
|
|
3102
|
+
inferExpressionType(expr.expression.expression)?.startsWith('*TnMap[')) {
|
|
3103
|
+
const mapType = inferExpressionType(expr.expression.expression);
|
|
3104
|
+
return {
|
|
3105
|
+
map: visit(expr.expression.expression),
|
|
3106
|
+
key: toGoValueOfType(expr.arguments[0], extractMapKeyType(mapType)),
|
|
3107
|
+
mapNode: expr.expression.expression
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
if (isElementAccessExpression(expr) && inferExpressionType(expr.expression)?.startsWith('*TnMap[')) {
|
|
3111
|
+
const mapType = inferExpressionType(expr.expression);
|
|
3112
|
+
return {
|
|
3113
|
+
map: visit(expr.expression),
|
|
3114
|
+
key: toGoValueOfType(expr.argumentExpression, extractMapKeyType(mapType)),
|
|
3115
|
+
mapNode: expr.expression
|
|
3116
|
+
};
|
|
1406
3117
|
}
|
|
1407
|
-
return
|
|
3118
|
+
return undefined;
|
|
3119
|
+
}
|
|
3120
|
+
function isNilableGoType(goType) {
|
|
3121
|
+
return (goType.startsWith('*') ||
|
|
3122
|
+
goType.startsWith('[]') ||
|
|
3123
|
+
goType.startsWith('map[') ||
|
|
3124
|
+
goType.startsWith('func') ||
|
|
3125
|
+
goType.startsWith('chan ') ||
|
|
3126
|
+
goType === 'interface{}');
|
|
1408
3127
|
}
|
|
1409
3128
|
function visitOptionalPropertyAccess(node) {
|
|
1410
3129
|
const baseExpr = visit(node.expression);
|
|
@@ -1414,7 +3133,10 @@ function visitOptionalPropertyAccess(node) {
|
|
|
1414
3133
|
return getAcessString(baseExpr, visit(node.name), objectType);
|
|
1415
3134
|
}
|
|
1416
3135
|
const className = baseType.replace(/^\*/, '').replace(/\[.*\]$/, '');
|
|
1417
|
-
const propertyType = classPropertyTypes.get(className)?.get(node.name.text) ??
|
|
3136
|
+
const propertyType = classPropertyTypes.get(className)?.get(node.name.text) ??
|
|
3137
|
+
interfacePropertyTypes.get(className)?.get(node.name.text) ??
|
|
3138
|
+
getStructFieldGoType(className, node.name.text) ??
|
|
3139
|
+
'interface{}';
|
|
1418
3140
|
const nullableType = makeNullableType(propertyType);
|
|
1419
3141
|
const tmp = getTempName('opt');
|
|
1420
3142
|
const propertyAccess = `${tmp}.${visit(node.name)}`;
|
|
@@ -1427,8 +3149,9 @@ function visitOptionalPropertyAccess(node) {
|
|
|
1427
3149
|
function visitOptionalElementAccess(node) {
|
|
1428
3150
|
const baseExpr = visit(node.expression);
|
|
1429
3151
|
const baseType = inferExpressionType(node.expression);
|
|
1430
|
-
if (!baseType || !baseType.startsWith('*')) {
|
|
1431
|
-
|
|
3152
|
+
if (!baseType || !baseType.startsWith('*') || baseType.startsWith('*TnMap[')) {
|
|
3153
|
+
const plainAccess = { ...node, questionDotToken: undefined };
|
|
3154
|
+
return visit(plainAccess);
|
|
1432
3155
|
}
|
|
1433
3156
|
const valueType = inferExpectedTypeFromContext(node) ?? 'interface{}';
|
|
1434
3157
|
const nullableType = makeNullableType(valueType);
|
|
@@ -1440,6 +3163,33 @@ function visitOptionalElementAccess(node) {
|
|
|
1440
3163
|
}
|
|
1441
3164
|
return `func() ${nullableType} { ${tmp} := ${baseExpr}; if ${tmp} == nil { var __zero ${nullableType}; return __zero }; return ${elementExpr} }()`;
|
|
1442
3165
|
}
|
|
3166
|
+
// The call with its receiver replaced by a narrowed temporary of the receiver's type
|
|
3167
|
+
function makeNarrowedReceiverCall(node, tmp, baseType) {
|
|
3168
|
+
variableGoTypes.set(tmp, baseType);
|
|
3169
|
+
const receiver = { kind: 'Identifier', text: tmp };
|
|
3170
|
+
const access = { ...node.expression, questionDotToken: undefined, expression: receiver };
|
|
3171
|
+
const call = { ...node, questionDotToken: undefined, expression: access, parent: node.parent };
|
|
3172
|
+
receiver.parent = access;
|
|
3173
|
+
access.parent = call;
|
|
3174
|
+
return call;
|
|
3175
|
+
}
|
|
3176
|
+
function visitNullablePrimitiveOptionalCall(node, baseExpr, baseType) {
|
|
3177
|
+
const tmp = getTempName('optc');
|
|
3178
|
+
const call = makeNarrowedReceiverCall(node, tmp, baseType);
|
|
3179
|
+
const resultType = makeNullableType(withNarrowingType([tmp], () => inferExpressionType(call)) ?? 'interface{}');
|
|
3180
|
+
const callCode = withNarrowing([tmp], () => visit(call));
|
|
3181
|
+
const value = resultType.startsWith('*') ? `func() ${resultType} { v := ${callCode}; return &v }()` : callCode;
|
|
3182
|
+
return `func() ${resultType} { ${tmp} := ${baseExpr}; if ${tmp} == nil { return nil }; return ${value} }()`;
|
|
3183
|
+
}
|
|
3184
|
+
function withNarrowingType(names, infer) {
|
|
3185
|
+
const added = names.filter((n) => !narrowedVariables.has(n));
|
|
3186
|
+
for (const name of added)
|
|
3187
|
+
narrowedVariables.add(name);
|
|
3188
|
+
const type = infer();
|
|
3189
|
+
for (const name of added)
|
|
3190
|
+
narrowedVariables.delete(name);
|
|
3191
|
+
return type;
|
|
3192
|
+
}
|
|
1443
3193
|
function visitOptionalCall(node) {
|
|
1444
3194
|
if (!isPropertyAccessExpression(node.expression)) {
|
|
1445
3195
|
return `${visit(node.expression)}(${(node.arguments ?? []).map((a) => visit(a)).join(', ')})`;
|
|
@@ -1449,8 +3199,14 @@ function visitOptionalCall(node) {
|
|
|
1449
3199
|
const baseExpr = visit(baseNode);
|
|
1450
3200
|
const baseType = inferExpressionType(baseNode);
|
|
1451
3201
|
const args = (node.arguments ?? []).map((a) => visit(a)).join(', ');
|
|
1452
|
-
if (
|
|
1453
|
-
return
|
|
3202
|
+
if (baseType && NULLABLE_PRIMITIVE_TYPES.includes(baseType)) {
|
|
3203
|
+
return visitNullablePrimitiveOptionalCall(node, baseExpr, baseType);
|
|
3204
|
+
}
|
|
3205
|
+
if (!baseType || !baseType.startsWith('*') || baseType.startsWith('*TnMap[')) {
|
|
3206
|
+
const plainAccess = { ...node.expression, questionDotToken: undefined };
|
|
3207
|
+
const plainCall = { ...node, questionDotToken: undefined, expression: plainAccess };
|
|
3208
|
+
plainAccess.parent = plainCall;
|
|
3209
|
+
return visit(plainCall);
|
|
1454
3210
|
}
|
|
1455
3211
|
const className = baseType.replace(/^\*/, '').replace(/\[.*\]$/, '');
|
|
1456
3212
|
const returnType = classMethodReturnTypes.get(className)?.get(methodName) ?? 'interface{}';
|
|
@@ -1479,7 +3235,7 @@ function inferArrayCallbackReturnType(callback, elementType, fallbackType) {
|
|
|
1479
3235
|
return explicitType || fallbackType;
|
|
1480
3236
|
}
|
|
1481
3237
|
if (isBlock(callback.body)) {
|
|
1482
|
-
return fallbackType;
|
|
3238
|
+
return inferFunctionBodyReturnType(callback) ?? fallbackType;
|
|
1483
3239
|
}
|
|
1484
3240
|
const inferred = inferExpressionType(callback.body);
|
|
1485
3241
|
return inferred ?? fallbackType;
|
|
@@ -1491,23 +3247,24 @@ function inferArrayCallbackReturnType(callback, elementType, fallbackType) {
|
|
|
1491
3247
|
}
|
|
1492
3248
|
return fallbackType;
|
|
1493
3249
|
}
|
|
1494
|
-
function buildArrayCallbackInfo(callback, elementType, forcedReturnType) {
|
|
3250
|
+
function buildArrayCallbackInfo(callback, elementType, forcedReturnType, paramTypes = [elementType, 'float64', `[]${elementType}`]) {
|
|
1495
3251
|
if (isArrowFunction(callback) || isFunctionExpression(callback)) {
|
|
1496
|
-
const paramCount = callback.parameters.length;
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
if (paramCount > 2) {
|
|
1506
|
-
params.push(`${visit(callback.parameters[2].name)} []${elementType}`);
|
|
1507
|
-
}
|
|
3252
|
+
const paramCount = (callback.parameters ?? []).length;
|
|
3253
|
+
(callback.parameters ?? []).slice(0, paramTypes.length).forEach((p, index) => {
|
|
3254
|
+
if (isIdentifier(p.name))
|
|
3255
|
+
registerLocalVariable(p.name.text, paramTypes[index]);
|
|
3256
|
+
});
|
|
3257
|
+
const callbackReturnType = forcedReturnType ?? inferArrayCallbackReturnType(callback, elementType, elementType);
|
|
3258
|
+
const params = (callback.parameters ?? [])
|
|
3259
|
+
.slice(0, paramTypes.length)
|
|
3260
|
+
.map((p, index) => `${visit(p.name)} ${paramTypes[index]}`);
|
|
1508
3261
|
const body = isBlock(callback.body)
|
|
1509
3262
|
? visit(callback.body, { inline: true })
|
|
1510
|
-
:
|
|
3263
|
+
: forcedReturnType === 'bool'
|
|
3264
|
+
? `{ return ${toGoCondition(callback.body)}; }`
|
|
3265
|
+
: forcedReturnType === ''
|
|
3266
|
+
? `{ ${visit(unwrapParentheses(callback.body))}; }`
|
|
3267
|
+
: `{ return ${visit(callback.body)}; }`;
|
|
1511
3268
|
return {
|
|
1512
3269
|
fnExpr: `func(${params.join(', ')}) ${callbackReturnType} ${body}`,
|
|
1513
3270
|
paramCount,
|
|
@@ -1528,7 +3285,12 @@ function buildArrayCallbackInvocation(callbackInfo, itemVar, indexVar, arrayVar)
|
|
|
1528
3285
|
args.push(`float64(${indexVar})`);
|
|
1529
3286
|
if (callbackInfo.paramCount > 2)
|
|
1530
3287
|
args.push(arrayVar);
|
|
1531
|
-
|
|
3288
|
+
const call = `(${callbackInfo.fnExpr})(${args.join(', ')})`;
|
|
3289
|
+
if (callbackInfo.paramCount > 0)
|
|
3290
|
+
return call;
|
|
3291
|
+
const returnType = callbackInfo.returnType ? ` ${callbackInfo.returnType}` : '';
|
|
3292
|
+
const ret = callbackInfo.returnType ? 'return ' : '';
|
|
3293
|
+
return `func()${returnType} { _ = ${itemVar}; ${ret}${call} }()`;
|
|
1532
3294
|
}
|
|
1533
3295
|
// Like buildArrayCallbackInvocation but for reduce: (acc, item, idx, arr)
|
|
1534
3296
|
function buildArrayCallbackInvocationReduce(callbackInfo, accVar, itemVar, indexVar, arrayVar) {
|
|
@@ -1556,6 +3318,21 @@ function visitArrayHigherOrderCall(node) {
|
|
|
1556
3318
|
const elementType = isArrayLikeGoType(ownerType)
|
|
1557
3319
|
? getArrayElementTypeFromGoType(ownerType)
|
|
1558
3320
|
: 'interface{}';
|
|
3321
|
+
if (methodName === 'forEach' && ownerType?.startsWith('*TnMap[')) {
|
|
3322
|
+
const callback = (node.arguments ?? [])[0];
|
|
3323
|
+
if (!callback)
|
|
3324
|
+
return undefined;
|
|
3325
|
+
const keyType = extractMapKeyType(ownerType);
|
|
3326
|
+
const valueType = extractMapValueType(ownerType);
|
|
3327
|
+
const info = buildArrayCallbackInfo(callback, valueType, '', [
|
|
3328
|
+
valueType === 'struct{}' ? keyType : valueType,
|
|
3329
|
+
keyType
|
|
3330
|
+
]);
|
|
3331
|
+
const key = getTempName('key');
|
|
3332
|
+
const value = getTempName('value');
|
|
3333
|
+
const args = [valueType === 'struct{}' ? key : value, key].slice(0, info.paramCount).join(', ');
|
|
3334
|
+
return `func() { for ${key}, ${value} := range ${arrayExpr}.All() { _, _ = ${key}, ${value}; (${info.fnExpr})(${args}) } }()`;
|
|
3335
|
+
}
|
|
1559
3336
|
if (methodName === 'join') {
|
|
1560
3337
|
// Only intercept array.join — if owner type is unknown/not array, fall through to callHandlers
|
|
1561
3338
|
if (!isArrayLikeGoType(ownerType))
|
|
@@ -1575,7 +3352,7 @@ function visitArrayHigherOrderCall(node) {
|
|
|
1575
3352
|
const idxVar = getTempName('i');
|
|
1576
3353
|
const itemVar = getTempName('item');
|
|
1577
3354
|
if (methodName === 'map') {
|
|
1578
|
-
const callbackInfo = buildArrayCallbackInfo(callback, elementType
|
|
3355
|
+
const callbackInfo = buildArrayCallbackInfo(callback, elementType);
|
|
1579
3356
|
const mappedType = callbackInfo.returnType || 'interface{}';
|
|
1580
3357
|
const resultVar = getTempName('mapres');
|
|
1581
3358
|
const rangeIndexVar = callbackInfo.paramCount > 1 ? idxVar : '_';
|
|
@@ -1622,7 +3399,12 @@ function visitArrayHigherOrderCall(node) {
|
|
|
1622
3399
|
: elementType;
|
|
1623
3400
|
const accVar = getTempName('acc');
|
|
1624
3401
|
// Build a callback that takes (acc, item, idx, arr) — param count determines what gets passed
|
|
1625
|
-
const cbInfo = buildArrayCallbackInfo(reduceCallback, elementType, accType
|
|
3402
|
+
const cbInfo = buildArrayCallbackInfo(reduceCallback, elementType, accType, [
|
|
3403
|
+
accType,
|
|
3404
|
+
elementType,
|
|
3405
|
+
'float64',
|
|
3406
|
+
`[]${elementType}`
|
|
3407
|
+
]);
|
|
1626
3408
|
const cbCall = buildArrayCallbackInvocationReduce(cbInfo, accVar, itemVar, idxVar, arrVar);
|
|
1627
3409
|
// Only include the index loop var if the callback uses it (paramCount > 2)
|
|
1628
3410
|
const reduceIdxVar = cbInfo.paramCount > 2 ? idxVar : '_';
|
|
@@ -1736,10 +3518,17 @@ function visitEnumDeclaration(node) {
|
|
|
1736
3518
|
function getType(typeNode, getArrayType = false) {
|
|
1737
3519
|
if (!typeNode)
|
|
1738
3520
|
return ':';
|
|
3521
|
+
if (typeNode.kind === 'ParenthesizedType')
|
|
3522
|
+
return getType(typeNode.type, getArrayType);
|
|
1739
3523
|
if (isArrayTypeNode(typeNode)) {
|
|
1740
3524
|
const elementType = getType(typeNode.elementType);
|
|
1741
3525
|
return getArrayType ? elementType : `[]${elementType}`;
|
|
1742
3526
|
}
|
|
3527
|
+
// [A, B] tuples are slices: []A when all elements share a type, else []interface{}
|
|
3528
|
+
if (typeNode.kind === 'TupleType') {
|
|
3529
|
+
const elementType = getTupleElementType(typeNode);
|
|
3530
|
+
return getArrayType ? elementType : `[]${elementType}`;
|
|
3531
|
+
}
|
|
1743
3532
|
// Handle union types (e.g. string | null, number | undefined)
|
|
1744
3533
|
if (isUnionTypeNode(typeNode)) {
|
|
1745
3534
|
const nonNullTypes = typeNode.types.filter((t) => t.kind !== 'NullKeyword' &&
|
|
@@ -1748,7 +3537,7 @@ function getType(typeNode, getArrayType = false) {
|
|
|
1748
3537
|
if (nonNullTypes.length === 1 && nonNullTypes.length < typeNode.types.length) {
|
|
1749
3538
|
// This is a nullable type T | null or T | undefined
|
|
1750
3539
|
const innerType = getType(nonNullTypes[0]);
|
|
1751
|
-
if (['float64', 'string', 'bool'].includes(innerType)) {
|
|
3540
|
+
if (['float64', 'string', 'bool'].includes(innerType) || isStructGoType(innerType)) {
|
|
1752
3541
|
return `*${innerType}`;
|
|
1753
3542
|
}
|
|
1754
3543
|
// Pointer/interface types already support nil
|
|
@@ -1758,7 +3547,7 @@ function getType(typeNode, getArrayType = false) {
|
|
|
1758
3547
|
return 'interface{}';
|
|
1759
3548
|
}
|
|
1760
3549
|
if (isFunctionTypeNode(typeNode)) {
|
|
1761
|
-
const params = typeNode.parameters
|
|
3550
|
+
const params = (typeNode.parameters ?? [])
|
|
1762
3551
|
.map((p) => (p.type ? getType(p.type) : 'interface{}'))
|
|
1763
3552
|
.join(', ');
|
|
1764
3553
|
const ret = typeNode.type ? ` ${getType(typeNode.type)}` : '';
|
|
@@ -1770,7 +3559,7 @@ function getType(typeNode, getArrayType = false) {
|
|
|
1770
3559
|
return getSafeName(name);
|
|
1771
3560
|
}
|
|
1772
3561
|
const aliasType = getAliasType(name);
|
|
1773
|
-
if (aliasType) {
|
|
3562
|
+
if (aliasType && aliasType.kind !== 'TypeLiteral') {
|
|
1774
3563
|
return getType(aliasType, getArrayType);
|
|
1775
3564
|
}
|
|
1776
3565
|
if (name === 'Promise' && typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
@@ -1779,21 +3568,30 @@ function getType(typeNode, getArrayType = false) {
|
|
|
1779
3568
|
if (name === 'RegExp') {
|
|
1780
3569
|
return '*regexp.Regexp';
|
|
1781
3570
|
}
|
|
1782
|
-
if (name === '
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
3571
|
+
if (name === 'RegExpExecArray' || name === 'RegExpMatchArray') {
|
|
3572
|
+
return '[]string';
|
|
3573
|
+
}
|
|
3574
|
+
if ((name === 'Map' || name === 'Record') &&
|
|
3575
|
+
typeNode.typeArguments &&
|
|
3576
|
+
typeNode.typeArguments.length === 2) {
|
|
3577
|
+
return mapGoType(getType(typeNode.typeArguments[0]), getType(typeNode.typeArguments[1]));
|
|
1786
3578
|
}
|
|
1787
3579
|
if (name === 'Set' && typeNode.typeArguments && typeNode.typeArguments.length === 1) {
|
|
1788
|
-
|
|
1789
|
-
return `map[${elementType}]struct{}`;
|
|
3580
|
+
return mapGoType(getType(typeNode.typeArguments[0]), 'struct{}');
|
|
1790
3581
|
}
|
|
1791
3582
|
const typeArgs = getTypeArguments(typeNode.typeArguments);
|
|
1792
|
-
if (classNames.has(name)) {
|
|
3583
|
+
if (classNames.has(name) || isObjectTypeName(name)) {
|
|
1793
3584
|
return `*${name}${typeArgs}`;
|
|
1794
3585
|
}
|
|
1795
3586
|
return `${name}${typeArgs}`;
|
|
1796
3587
|
}
|
|
3588
|
+
// { a: T; b?: U } → struct{ a T; b *U }
|
|
3589
|
+
if (typeNode.kind === 'TypeLiteral') {
|
|
3590
|
+
const fields = (typeNode.members ?? [])
|
|
3591
|
+
.filter((m) => isPropertySignature(m) && isIdentifier(m.name))
|
|
3592
|
+
.map((m) => `${goFieldName(m.name.text)} ${getOptionalNodeType(m.type, !!m.questionToken)}`);
|
|
3593
|
+
return fields.length > 0 ? `*struct{ ${fields.join('; ')} }` : 'interface{}';
|
|
3594
|
+
}
|
|
1797
3595
|
// Syntactic replacement for the ts typechecker: render the type node's text
|
|
1798
3596
|
let typeName = typeNodeToText(typeNode);
|
|
1799
3597
|
const isArray = typeName.includes('[]');
|
|
@@ -1811,7 +3609,7 @@ function getType(typeNode, getArrayType = false) {
|
|
|
1811
3609
|
case 'boolean':
|
|
1812
3610
|
return 'bool';
|
|
1813
3611
|
case 'any':
|
|
1814
|
-
return '
|
|
3612
|
+
return 'interface{}';
|
|
1815
3613
|
case 'void':
|
|
1816
3614
|
return '';
|
|
1817
3615
|
default:
|
|
@@ -1864,11 +3662,25 @@ function getClassNameFromTypeNode(typeNode) {
|
|
|
1864
3662
|
}
|
|
1865
3663
|
function resolveExpressionType(expr) {
|
|
1866
3664
|
if (isIdentifier(expr)) {
|
|
1867
|
-
return variableTypes.get(expr.text);
|
|
3665
|
+
return variableTypes.get(expr.text) ?? goTypeCategory(variableGoTypes.get(expr.text));
|
|
1868
3666
|
}
|
|
1869
3667
|
if (expr.kind === 'ThisKeyword') {
|
|
1870
3668
|
return 'class';
|
|
1871
3669
|
}
|
|
3670
|
+
return goTypeCategory(inferExpressionType(expr));
|
|
3671
|
+
}
|
|
3672
|
+
// Method-dispatch category of an inferred Go type (e.g. `const s = new Set<T>()`)
|
|
3673
|
+
function goTypeCategory(goType) {
|
|
3674
|
+
if (!goType)
|
|
3675
|
+
return undefined;
|
|
3676
|
+
if (goType.startsWith('*TnMap['))
|
|
3677
|
+
return extractMapValueType(goType) === 'struct{}' ? 'Set' : 'Map';
|
|
3678
|
+
if (goType.startsWith('[]'))
|
|
3679
|
+
return 'array';
|
|
3680
|
+
if (goType === 'string')
|
|
3681
|
+
return 'string';
|
|
3682
|
+
if (goType === '*regexp.Regexp')
|
|
3683
|
+
return 'RegExp';
|
|
1872
3684
|
return undefined;
|
|
1873
3685
|
}
|
|
1874
3686
|
function isNilLiteral(node) {
|
|
@@ -1883,7 +3695,7 @@ function getAcessString(leftSide, rightSide, objectType) {
|
|
|
1883
3695
|
return `float64(len(${leftSide}))`;
|
|
1884
3696
|
}
|
|
1885
3697
|
if (rightSide === 'size' && (objectType === 'Map' || objectType === 'Set')) {
|
|
1886
|
-
return `float64(
|
|
3698
|
+
return `float64(${leftSide}.Len())`;
|
|
1887
3699
|
}
|
|
1888
3700
|
// process global properties
|
|
1889
3701
|
if (leftSide === 'process') {
|
|
@@ -1892,16 +3704,32 @@ function getAcessString(leftSide, rightSide, objectType) {
|
|
|
1892
3704
|
return 'os.Args';
|
|
1893
3705
|
}
|
|
1894
3706
|
if (rightSide === 'platform') {
|
|
1895
|
-
|
|
1896
|
-
|
|
3707
|
+
// Node's names: win32 / darwin / linux
|
|
3708
|
+
useHelper('osPlatform');
|
|
3709
|
+
return 'TnOsPlatform()';
|
|
1897
3710
|
}
|
|
1898
3711
|
if (rightSide === 'env') {
|
|
1899
3712
|
// process.env.X is detected via the nested access below
|
|
1900
3713
|
return 'process.env';
|
|
1901
3714
|
}
|
|
1902
3715
|
}
|
|
3716
|
+
if (leftSide === 'Number') {
|
|
3717
|
+
const constants = {
|
|
3718
|
+
MAX_SAFE_INTEGER: 'float64(9007199254740991)',
|
|
3719
|
+
MIN_SAFE_INTEGER: 'float64(-9007199254740991)',
|
|
3720
|
+
EPSILON: 'float64(2.220446049250313e-16)'
|
|
3721
|
+
};
|
|
3722
|
+
if (constants[rightSide])
|
|
3723
|
+
return constants[rightSide];
|
|
3724
|
+
if (rightSide === 'MAX_VALUE' || rightSide === 'POSITIVE_INFINITY' || rightSide === 'NaN') {
|
|
3725
|
+
importedPackages.add('math');
|
|
3726
|
+
if (rightSide === 'MAX_VALUE')
|
|
3727
|
+
return 'math.MaxFloat64';
|
|
3728
|
+
return rightSide === 'NaN' ? 'math.NaN()' : 'math.Inf(1)';
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
1903
3731
|
// process.env.X → TnGetenv("X") (os.Environ() is a []string in Go, not a map)
|
|
1904
|
-
if (leftSide === 'process.env') {
|
|
3732
|
+
if (leftSide === 'process.env' || leftSide === '(process.env)') {
|
|
1905
3733
|
useHelper('getenv');
|
|
1906
3734
|
return `TnGetenv("${rightSide}")`;
|
|
1907
3735
|
}
|
|
@@ -1945,13 +3773,20 @@ const callHandlers = {
|
|
|
1945
3773
|
importedPackages.add('math');
|
|
1946
3774
|
return `math.Abs(${args[0]})`;
|
|
1947
3775
|
},
|
|
3776
|
+
// Math.max(...arr) → slices.Max(arr); otherwise Go's variadic builtin
|
|
1948
3777
|
'Math.max': (_caller, args) => {
|
|
1949
|
-
|
|
1950
|
-
|
|
3778
|
+
if (args.length === 1 && args[0].endsWith('...')) {
|
|
3779
|
+
importedPackages.add('slices');
|
|
3780
|
+
return `slices.Max(${args[0].slice(0, -3)})`;
|
|
3781
|
+
}
|
|
3782
|
+
return `max(${args.join(', ')})`;
|
|
1951
3783
|
},
|
|
1952
3784
|
'Math.min': (_caller, args) => {
|
|
1953
|
-
|
|
1954
|
-
|
|
3785
|
+
if (args.length === 1 && args[0].endsWith('...')) {
|
|
3786
|
+
importedPackages.add('slices');
|
|
3787
|
+
return `slices.Min(${args[0].slice(0, -3)})`;
|
|
3788
|
+
}
|
|
3789
|
+
return `min(${args.join(', ')})`;
|
|
1955
3790
|
},
|
|
1956
3791
|
'Math.sqrt': (_caller, args) => {
|
|
1957
3792
|
importedPackages.add('math');
|
|
@@ -1962,12 +3797,12 @@ const callHandlers = {
|
|
|
1962
3797
|
return `math.Pow(${args[0]}, ${args[1]})`;
|
|
1963
3798
|
},
|
|
1964
3799
|
parseInt: (_caller, args) => {
|
|
1965
|
-
|
|
1966
|
-
return `
|
|
3800
|
+
useHelper('dynamic');
|
|
3801
|
+
return `TnParseInt(${args[0]}, ${args[1] ?? '10'})`;
|
|
1967
3802
|
},
|
|
1968
3803
|
parseFloat: (_caller, args) => {
|
|
1969
|
-
|
|
1970
|
-
return `
|
|
3804
|
+
useHelper('dynamic');
|
|
3805
|
+
return `TnParseFloat(${args[0]})`;
|
|
1971
3806
|
},
|
|
1972
3807
|
'process.exit': (_caller, args) => {
|
|
1973
3808
|
importedPackages.add('os');
|
|
@@ -1978,27 +3813,17 @@ const callHandlers = {
|
|
|
1978
3813
|
return `func() string { __d, _ := os.Getwd(); return __d }()`;
|
|
1979
3814
|
},
|
|
1980
3815
|
'JSON.stringify': (_caller, args) => {
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
return `func() string { __b, _ := json.MarshalIndent(${args[0]}, "", " "); return string(__b) }()`;
|
|
1984
|
-
}
|
|
1985
|
-
return `func() string { __b, _ := json.Marshal(${args[0]}); return string(__b) }()`;
|
|
3816
|
+
useHelper('jsonStringify');
|
|
3817
|
+
return `TnJSONStringify(${args[0]}, ${args.length >= 3 ? '" "' : '""'})`;
|
|
1986
3818
|
},
|
|
1987
3819
|
'JSON.parse': (_caller, args) => {
|
|
1988
3820
|
importedPackages.add('encoding/json');
|
|
1989
3821
|
return `func() interface{} { var __v interface{}; json.Unmarshal([]byte(${args[0]}), &__v); return __v }()`;
|
|
1990
3822
|
},
|
|
1991
|
-
'Object.keys': (_caller, args) => {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
}
|
|
1995
|
-
'Object.values': (_caller, args) => {
|
|
1996
|
-
return `func() []interface{} { __v := make([]interface{}, 0); for _, v := range ${args[0]} { __v = append(__v, v) }; return __v }()`;
|
|
1997
|
-
},
|
|
1998
|
-
'Object.entries': (_caller, args) => {
|
|
1999
|
-
// When used outside for...of, produce a slice of [key, value] pairs — rarely needed
|
|
2000
|
-
return args[0];
|
|
2001
|
-
},
|
|
3823
|
+
'Object.keys': (_caller, args) => `${args[0]}.Keys()`,
|
|
3824
|
+
'Object.values': (_caller, args) => `${args[0]}.Values()`,
|
|
3825
|
+
// Outside for...of (which iterates the map directly): [key, value] pairs
|
|
3826
|
+
'Object.entries': (_caller, args) => `${args[0]}.Entries()`,
|
|
2002
3827
|
'Math.log': (_caller, args) => {
|
|
2003
3828
|
importedPackages.add('math');
|
|
2004
3829
|
return `math.Log(${args[0]})`;
|
|
@@ -2035,13 +3860,38 @@ const callHandlers = {
|
|
|
2035
3860
|
importedPackages.add('os');
|
|
2036
3861
|
return `fmt.Fprintln(os.Stderr, ${args.join(', ')})`;
|
|
2037
3862
|
},
|
|
3863
|
+
'console.warn': (_caller, args) => {
|
|
3864
|
+
importedPackages.add('fmt');
|
|
3865
|
+
importedPackages.add('os');
|
|
3866
|
+
return `fmt.Fprintln(os.Stderr, ${args.join(', ')})`;
|
|
3867
|
+
},
|
|
3868
|
+
'Array.isArray': (_caller, args) => {
|
|
3869
|
+
importedPackages.add('reflect');
|
|
3870
|
+
return `(reflect.ValueOf(${args[0]}).Kind() == reflect.Slice)`;
|
|
3871
|
+
},
|
|
2038
3872
|
'String': (_caller, args) => {
|
|
2039
3873
|
importedPackages.add('fmt');
|
|
2040
3874
|
return `fmt.Sprintf("%v", ${args[0]})`;
|
|
2041
3875
|
},
|
|
2042
3876
|
'Number': (_caller, args) => {
|
|
2043
|
-
|
|
2044
|
-
return `
|
|
3877
|
+
useHelper('dynamic');
|
|
3878
|
+
return `TnNumber(${args[0]})`;
|
|
3879
|
+
},
|
|
3880
|
+
'Number.isNaN': (_caller, args) => {
|
|
3881
|
+
importedPackages.add('math');
|
|
3882
|
+
return `math.IsNaN(${args[0]})`;
|
|
3883
|
+
},
|
|
3884
|
+
isNaN: (_caller, args) => {
|
|
3885
|
+
importedPackages.add('math');
|
|
3886
|
+
return `math.IsNaN(${args[0]})`;
|
|
3887
|
+
},
|
|
3888
|
+
'Number.isFinite': (_caller, args) => {
|
|
3889
|
+
importedPackages.add('math');
|
|
3890
|
+
return `!math.IsInf(${args[0]}, 0) && !math.IsNaN(${args[0]})`;
|
|
3891
|
+
},
|
|
3892
|
+
'Number.isInteger': (_caller, args) => {
|
|
3893
|
+
importedPackages.add('math');
|
|
3894
|
+
return `(${args[0]} == math.Trunc(${args[0]}) && !math.IsInf(${args[0]}, 0))`;
|
|
2045
3895
|
},
|
|
2046
3896
|
'Boolean': (_caller, args) => {
|
|
2047
3897
|
return `(${args[0]} != nil && ${args[0]} != false && ${args[0]} != 0 && ${args[0]} != "")`;
|
|
@@ -2049,6 +3899,8 @@ const callHandlers = {
|
|
|
2049
3899
|
};
|
|
2050
3900
|
const stringMethodHandlers = {
|
|
2051
3901
|
split: (obj, args) => {
|
|
3902
|
+
if (args[0]?.startsWith('regexp.MustCompile('))
|
|
3903
|
+
return `${args[0]}.Split(${obj}, -1)`;
|
|
2052
3904
|
importedPackages.add('strings');
|
|
2053
3905
|
return `strings.Split(${obj}, ${args[0]})`;
|
|
2054
3906
|
},
|
|
@@ -2103,17 +3955,16 @@ const stringMethodHandlers = {
|
|
|
2103
3955
|
charAt: (obj, args) => `string(${obj}[int(${args[0]})])`,
|
|
2104
3956
|
substring: (obj, args) => {
|
|
2105
3957
|
if (args.length >= 2)
|
|
2106
|
-
return `${obj}[
|
|
2107
|
-
return `${obj}[
|
|
3958
|
+
return `${obj}[${sliceIndex(obj, args[0])}:${sliceIndex(obj, args[1])}]`;
|
|
3959
|
+
return `${obj}[${sliceIndex(obj, args[0])}:]`;
|
|
2108
3960
|
},
|
|
2109
3961
|
slice: (obj, args) => {
|
|
2110
3962
|
if (args.length >= 2)
|
|
2111
|
-
return `${obj}[
|
|
2112
|
-
return `${obj}[
|
|
3963
|
+
return `${obj}[${sliceIndex(obj, args[0])}:${sliceIndex(obj, args[1])}]`;
|
|
3964
|
+
return `${obj}[${sliceIndex(obj, args[0])}:]`;
|
|
2113
3965
|
},
|
|
2114
3966
|
concat: (obj, args) => `${obj} + ${args.join(' + ')}`,
|
|
2115
3967
|
padStart: (obj, args) => {
|
|
2116
|
-
importedPackages.add('fmt');
|
|
2117
3968
|
importedPackages.add('strings');
|
|
2118
3969
|
const pad = args[1] ?? '" "';
|
|
2119
3970
|
return `func() string { __s := ${obj}; __n := int(${args[0]}) - len(__s); if __n > 0 { __s = strings.Repeat(${pad}, __n)[:__n] + __s }; return __s }()`;
|
|
@@ -2123,17 +3974,12 @@ const stringMethodHandlers = {
|
|
|
2123
3974
|
const pad = args[1] ?? '" "';
|
|
2124
3975
|
return `func() string { __s := ${obj}; __n := int(${args[0]}) - len(__s); if __n > 0 { __s = __s + strings.Repeat(${pad}, __n)[:__n] }; return __s }()`;
|
|
2125
3976
|
},
|
|
2126
|
-
match: (obj, args) => {
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
return `regexp.MustCompile(${args[0]}).FindAllStringSubmatch(${obj}, -1)`;
|
|
2133
|
-
},
|
|
2134
|
-
search: (obj, args) => {
|
|
2135
|
-
importedPackages.add('regexp');
|
|
2136
|
-
return `float64(regexp.MustCompile(${args[0]}).FindStringIndex(${obj})[0])`;
|
|
3977
|
+
match: (obj, args) => `${toGoRegexp(args[0])}.FindStringSubmatch(${obj})`,
|
|
3978
|
+
matchAll: (obj, args) => `${toGoRegexp(args[0])}.FindAllStringSubmatch(${obj}, -1)`,
|
|
3979
|
+
search: (obj, args) => `func() float64 { __loc := ${toGoRegexp(args[0])}.FindStringIndex(${obj}); if __loc == nil { return -1 }; return float64(__loc[0]) }()`,
|
|
3980
|
+
lastIndexOf: (obj, args) => {
|
|
3981
|
+
importedPackages.add('strings');
|
|
3982
|
+
return `float64(strings.LastIndex(${obj}, ${args[0]}))`;
|
|
2137
3983
|
},
|
|
2138
3984
|
at: (obj, args) => {
|
|
2139
3985
|
return `func() string { __i := int(${args[0]}); if __i < 0 { __i = len(${obj}) + __i }; return string(${obj}[__i]) }()`;
|
|
@@ -2150,37 +3996,54 @@ const regexpMethodHandlers = {
|
|
|
2150
3996
|
const arrayMethodHandlers = {
|
|
2151
3997
|
push: (obj, args) => `${obj} = append(${obj}, ${args.join(', ')})`,
|
|
2152
3998
|
pop: (obj) => {
|
|
2153
|
-
|
|
3999
|
+
const elementType = receiverElementType();
|
|
4000
|
+
if (!isAddressable(obj)) {
|
|
4001
|
+
return `func() ${elementType} { __s := ${obj}; if len(__s) == 0 { var __zero ${elementType}; return __zero }; return __s[len(__s)-1] }()`;
|
|
4002
|
+
}
|
|
4003
|
+
return `func() ${elementType} { if len(${obj}) == 0 { var __zero ${elementType}; return __zero }; __last := ${obj}[len(${obj})-1]; ${obj} = ${obj}[:len(${obj})-1]; return __last }()`;
|
|
2154
4004
|
},
|
|
2155
4005
|
shift: (obj) => {
|
|
2156
|
-
|
|
4006
|
+
const elementType = receiverElementType();
|
|
4007
|
+
if (!isAddressable(obj)) {
|
|
4008
|
+
return `func() ${elementType} { __s := ${obj}; if len(__s) == 0 { var __zero ${elementType}; return __zero }; return __s[0] }()`;
|
|
4009
|
+
}
|
|
4010
|
+
return `func() ${elementType} { if len(${obj}) == 0 { var __zero ${elementType}; return __zero }; __first := ${obj}[0]; ${obj} = ${obj}[1:]; return __first }()`;
|
|
2157
4011
|
},
|
|
2158
|
-
unshift: (obj, args) => `${obj} = append([]
|
|
4012
|
+
unshift: (obj, args) => `${obj} = append([]${receiverElementType()}{${args.join(', ')}}, ${obj}...)`,
|
|
2159
4013
|
join: (obj, args) => {
|
|
2160
4014
|
importedPackages.add('strings');
|
|
2161
4015
|
return `strings.Join(${obj}, ${args[0] ?? '""'})`;
|
|
2162
4016
|
},
|
|
2163
4017
|
slice: (obj, args) => {
|
|
2164
4018
|
if (args.length >= 2)
|
|
2165
|
-
return `${obj}[
|
|
2166
|
-
return `${obj}[
|
|
4019
|
+
return `${obj}[${sliceIndex(obj, args[0])}:${sliceIndex(obj, args[1])}]`;
|
|
4020
|
+
return `${obj}[${sliceIndex(obj, args[0])}:]`;
|
|
2167
4021
|
},
|
|
2168
4022
|
reverse: (obj) => {
|
|
2169
4023
|
importedPackages.add('slices');
|
|
2170
4024
|
return `func() interface{} { slices.Reverse(${obj}); return nil }()`;
|
|
2171
4025
|
},
|
|
2172
|
-
|
|
4026
|
+
// Sorts in place and returns the array; without a comparator, compares as strings (like JS)
|
|
4027
|
+
sort: (obj, args) => {
|
|
2173
4028
|
importedPackages.add('sort');
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
return `func()
|
|
4029
|
+
const arrayType = currentReceiverGoType?.startsWith('[]') ? currentReceiverGoType : '[]interface{}';
|
|
4030
|
+
let less = `(${args[0]})(__s[i], __s[j]) < 0`;
|
|
4031
|
+
if (!args[0]) {
|
|
4032
|
+
importedPackages.add('fmt');
|
|
4033
|
+
less = `fmt.Sprintf("%v", __s[i]) < fmt.Sprintf("%v", __s[j])`;
|
|
4034
|
+
}
|
|
4035
|
+
return `func() ${arrayType} { __s := ${obj}; sort.SliceStable(__s, func(i, j int) bool { return ${less} }); return __s }()`;
|
|
2181
4036
|
},
|
|
4037
|
+
indexOf: (obj, args) => `func() float64 { __x := ${args[0]}; for __i, __v := range ${obj} { if ${elementEquals('__v', '__x')} { return float64(__i) } }; return float64(-1) }()`,
|
|
4038
|
+
includes: (obj, args) => `func() bool { __x := ${args[0]}; for _, __v := range ${obj} { if ${elementEquals('__v', '__x')} { return true } }; return false }()`,
|
|
2182
4039
|
concat: (obj, args) => `append(${obj}, ${args.join(', ')}...)`,
|
|
2183
|
-
|
|
4040
|
+
// [][]T → []T (one level, like JS's default depth)
|
|
4041
|
+
flat: (obj) => {
|
|
4042
|
+
const elementType = receiverElementType();
|
|
4043
|
+
if (!elementType.startsWith('[]'))
|
|
4044
|
+
return obj;
|
|
4045
|
+
return `func() ${elementType} { var __flat ${elementType}; for _, __inner := range ${obj} { __flat = append(__flat, __inner...) }; return __flat }()`;
|
|
4046
|
+
},
|
|
2184
4047
|
toString: (obj) => {
|
|
2185
4048
|
importedPackages.add('fmt');
|
|
2186
4049
|
return `fmt.Sprintf("%v", ${obj})`;
|
|
@@ -2190,24 +4053,52 @@ const arrayMethodHandlers = {
|
|
|
2190
4053
|
return `func() interface{} { __i := int(${args[0]}); if __i < 0 { __i = len(${obj}) + __i }; if __i < 0 || __i >= len(${obj}) { return nil }; return ${obj}[__i] }()`;
|
|
2191
4054
|
},
|
|
2192
4055
|
};
|
|
4056
|
+
function receiverElementType() {
|
|
4057
|
+
return currentReceiverGoType?.startsWith('[]') ? currentReceiverGoType.slice(2) : 'interface{}';
|
|
4058
|
+
}
|
|
4059
|
+
// Go expressions that can be assigned to (variables, fields, index expressions)
|
|
4060
|
+
function isAddressable(code) {
|
|
4061
|
+
return /^[\w.]+(\[[^\]]*\])*$/.test(code);
|
|
4062
|
+
}
|
|
4063
|
+
// A regex argument as a *regexp.Regexp: compiled literals pass through, pattern
|
|
4064
|
+
// strings are compiled
|
|
4065
|
+
function toGoRegexp(arg) {
|
|
4066
|
+
importedPackages.add('regexp');
|
|
4067
|
+
if (arg.startsWith('regexp.MustCompile(') || variableGoTypes.get(arg) === '*regexp.Regexp')
|
|
4068
|
+
return arg;
|
|
4069
|
+
return `regexp.MustCompile(${arg})`;
|
|
4070
|
+
}
|
|
4071
|
+
// A JS slice index: negative values count from the end
|
|
4072
|
+
// A JS slice index: negative values count from the end; out-of-range values clamp
|
|
4073
|
+
function sliceIndex(obj, index) {
|
|
4074
|
+
if (index.startsWith('-'))
|
|
4075
|
+
return `max(0, int(float64(len(${obj})) + ${index}))`;
|
|
4076
|
+
return `min(int(${index}), len(${obj}))`;
|
|
4077
|
+
}
|
|
4078
|
+
// Equality of two array elements of the current receiver's element type
|
|
4079
|
+
function elementEquals(a, b) {
|
|
4080
|
+
const elementType = receiverElementType();
|
|
4081
|
+
if (elementType === 'interface{}' || elementType.startsWith('*TnMap[') || elementType.startsWith('[]')) {
|
|
4082
|
+
useHelper('dynamic');
|
|
4083
|
+
return `TnSame(${a}, ${b})`;
|
|
4084
|
+
}
|
|
4085
|
+
return `${a} == ${b}`;
|
|
4086
|
+
}
|
|
2193
4087
|
const mapMethodHandlers = {
|
|
2194
|
-
set: (obj, args) => `${obj}
|
|
2195
|
-
get: (obj, args) => `${obj}
|
|
2196
|
-
has: (obj, args) => {
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
clear: (obj) => `clear(${obj})`
|
|
4088
|
+
set: (obj, args) => `${obj}.Set(${args[0]}, ${args[1]})`,
|
|
4089
|
+
get: (obj, args) => `${obj}.Get(${args[0]})`,
|
|
4090
|
+
has: (obj, args) => `${obj}.Has(${args[0]})`,
|
|
4091
|
+
delete: (obj, args) => `${obj}.Delete(${args[0]})`,
|
|
4092
|
+
clear: (obj) => `${obj}.Clear()`,
|
|
4093
|
+
keys: (obj) => `${obj}.Keys()`,
|
|
4094
|
+
values: (obj) => `${obj}.Values()`
|
|
2202
4095
|
};
|
|
2203
4096
|
const setMethodHandlers = {
|
|
2204
|
-
add: (obj, args) => `${obj}
|
|
2205
|
-
has: (obj, args) => {
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2209
|
-
delete: (obj, args) => `delete(${obj}, ${args[0]})`,
|
|
2210
|
-
clear: (obj) => `clear(${obj})`
|
|
4097
|
+
add: (obj, args) => `${obj}.Set(${args[0]}, struct{}{})`,
|
|
4098
|
+
has: (obj, args) => `${obj}.Has(${args[0]})`,
|
|
4099
|
+
delete: (obj, args) => `${obj}.Delete(${args[0]})`,
|
|
4100
|
+
clear: (obj) => `${obj}.Clear()`,
|
|
4101
|
+
values: (obj) => `${obj}.Keys()`
|
|
2211
4102
|
};
|
|
2212
4103
|
function getDynamicCallHandler(caller, objectType) {
|
|
2213
4104
|
if (promiseResolveName && caller === promiseResolveName) {
|
|
@@ -2218,10 +4109,20 @@ function getDynamicCallHandler(caller, objectType) {
|
|
|
2218
4109
|
const methodName = caller.substring(dotIndex + 1);
|
|
2219
4110
|
// toString() is universal — works for any type including numbers and objects
|
|
2220
4111
|
if (methodName === 'toString') {
|
|
2221
|
-
return (c) => {
|
|
4112
|
+
return (c, args) => {
|
|
2222
4113
|
const obj = c.substring(0, dotIndex);
|
|
2223
|
-
|
|
2224
|
-
|
|
4114
|
+
if (args.length > 0) {
|
|
4115
|
+
importedPackages.add('strconv');
|
|
4116
|
+
return `strconv.FormatInt(int64(${obj}), int(${args[0]}))`;
|
|
4117
|
+
}
|
|
4118
|
+
return jsStringOf(obj, objectType === 'number' || objectType === 'float64' ? 'float64' : undefined);
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
// number.toFixed(digits)
|
|
4122
|
+
if (methodName === 'toFixed') {
|
|
4123
|
+
return (c, args) => {
|
|
4124
|
+
importedPackages.add('strconv');
|
|
4125
|
+
return `strconv.FormatFloat(${c.substring(0, dotIndex)}, 'f', int(${args[0] ?? '0'}), 64)`;
|
|
2225
4126
|
};
|
|
2226
4127
|
}
|
|
2227
4128
|
// Class instances use their own methods — never intercept
|
|
@@ -2290,6 +4191,177 @@ function getOperatorText(operator) {
|
|
|
2290
4191
|
function getTimerName(name) {
|
|
2291
4192
|
return `__timer_${name.replaceAll(' ', '_').replaceAll('"', '')}__`;
|
|
2292
4193
|
}
|
|
4194
|
+
// typeof x: a constant when the Go type is known, a runtime check for any values
|
|
4195
|
+
function visitTypeOf(node) {
|
|
4196
|
+
const goType = inferExpressionType(node.expression);
|
|
4197
|
+
const known = { string: 'string', float64: 'number', bool: 'boolean', nil: 'undefined' };
|
|
4198
|
+
if (goType && known[goType])
|
|
4199
|
+
return toGoStringLiteral(known[goType]);
|
|
4200
|
+
if (goType?.startsWith('func'))
|
|
4201
|
+
return '"function"';
|
|
4202
|
+
if (goType && goType !== 'interface{}' && !NULLABLE_PRIMITIVE_TYPES.includes(goType))
|
|
4203
|
+
return '"object"';
|
|
4204
|
+
useHelper('typeOf');
|
|
4205
|
+
return `TnTypeOf(${visit(node.expression)})`;
|
|
4206
|
+
}
|
|
4207
|
+
// x.method(...) with an any receiver, as a call on the receiver cast to the type
|
|
4208
|
+
// that defines the method (array or string); undefined for other methods
|
|
4209
|
+
function castDynamicReceiverCall(node) {
|
|
4210
|
+
if (!isPropertyAccessExpression(node.expression) || isAsExpression(node.expression.expression)) {
|
|
4211
|
+
return undefined;
|
|
4212
|
+
}
|
|
4213
|
+
if (!isDynamicValue(node.expression.expression))
|
|
4214
|
+
return undefined;
|
|
4215
|
+
const method = node.expression.name.text;
|
|
4216
|
+
let castType;
|
|
4217
|
+
if (ARRAY_ONLY_METHODS.has(method))
|
|
4218
|
+
castType = { kind: 'ArrayType', elementType: { kind: 'AnyKeyword' } };
|
|
4219
|
+
else if (STRING_ONLY_METHODS.has(method))
|
|
4220
|
+
castType = { kind: 'StringKeyword' };
|
|
4221
|
+
if (!castType)
|
|
4222
|
+
return undefined;
|
|
4223
|
+
const cast = { kind: 'AsExpression', expression: node.expression.expression, type: castType };
|
|
4224
|
+
const access = { ...node.expression, expression: cast };
|
|
4225
|
+
const call = { ...node, expression: access };
|
|
4226
|
+
cast.parent = access;
|
|
4227
|
+
access.parent = call;
|
|
4228
|
+
return call;
|
|
4229
|
+
}
|
|
4230
|
+
function isObjectKeysOfDynamic(node) {
|
|
4231
|
+
return (isPropertyAccessExpression(node.expression) &&
|
|
4232
|
+
isIdentifier(node.expression.expression) &&
|
|
4233
|
+
node.expression.expression.text === 'Object' &&
|
|
4234
|
+
node.expression.name.text === 'keys' &&
|
|
4235
|
+
!!node.arguments?.[0] &&
|
|
4236
|
+
isDynamicValue(node.arguments[0]));
|
|
4237
|
+
}
|
|
4238
|
+
const ARRAY_ONLY_METHODS = new Set([
|
|
4239
|
+
'map', 'filter', 'some', 'every', 'find', 'findIndex', 'forEach', 'reduce', 'join', 'flat', 'sort', 'reverse'
|
|
4240
|
+
]);
|
|
4241
|
+
const STRING_ONLY_METHODS = new Set([
|
|
4242
|
+
'startsWith', 'endsWith', 'trim', 'trimStart', 'trimEnd', 'toUpperCase', 'toLowerCase', 'split',
|
|
4243
|
+
'replace', 'replaceAll', 'charAt', 'charCodeAt', 'padStart', 'padEnd', 'repeat', 'match', 'matchAll', 'search',
|
|
4244
|
+
'substring', 'lastIndexOf'
|
|
4245
|
+
]);
|
|
4246
|
+
// x.method(...) where x is any: re-visit with the receiver cast to the type the
|
|
4247
|
+
// method belongs to; methods of both strings and arrays dispatch at runtime
|
|
4248
|
+
function visitDynamicMethodCall(node) {
|
|
4249
|
+
if (isObjectKeysOfDynamic(node)) {
|
|
4250
|
+
useHelper('dynamic');
|
|
4251
|
+
return `TnKeys(${visit(node.arguments[0])})`;
|
|
4252
|
+
}
|
|
4253
|
+
if (!isPropertyAccessExpression(node.expression) || !isDynamicValue(node.expression.expression)) {
|
|
4254
|
+
return undefined;
|
|
4255
|
+
}
|
|
4256
|
+
const method = node.expression.name.text;
|
|
4257
|
+
const castCall = castDynamicReceiverCall(node);
|
|
4258
|
+
if (castCall)
|
|
4259
|
+
return visit(castCall);
|
|
4260
|
+
const dynamicHelpers = {
|
|
4261
|
+
includes: 'TnIncludes',
|
|
4262
|
+
indexOf: 'TnIndexOf',
|
|
4263
|
+
slice: 'TnSlice',
|
|
4264
|
+
concat: 'TnConcat'
|
|
4265
|
+
};
|
|
4266
|
+
const helper = dynamicHelpers[method];
|
|
4267
|
+
if (!helper)
|
|
4268
|
+
return undefined;
|
|
4269
|
+
useHelper('dynamic');
|
|
4270
|
+
const args = (node.arguments ?? []).map((a) => visit(a));
|
|
4271
|
+
return `${helper}(${[visit(node.expression.expression), ...args].join(', ')})`;
|
|
4272
|
+
}
|
|
4273
|
+
function isRegexReplaceCall(node) {
|
|
4274
|
+
if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression))
|
|
4275
|
+
return false;
|
|
4276
|
+
const method = node.expression.name.text;
|
|
4277
|
+
if (method !== 'replace' && method !== 'replaceAll')
|
|
4278
|
+
return false;
|
|
4279
|
+
const pattern = (node.arguments ?? [])[0];
|
|
4280
|
+
if (!pattern || (node.arguments ?? []).length < 2)
|
|
4281
|
+
return false;
|
|
4282
|
+
return (isRegularExpressionLiteral(pattern) ||
|
|
4283
|
+
inferExpressionType(pattern) === '*regexp.Regexp' ||
|
|
4284
|
+
resolveExpressionType(pattern) === 'RegExp');
|
|
4285
|
+
}
|
|
4286
|
+
// Element type of the array whose .sort(fn) receives this comparator
|
|
4287
|
+
function getSortComparatorElementType(fn) {
|
|
4288
|
+
const call = fn.parent;
|
|
4289
|
+
if (!isCallExpression(call) || call.arguments?.[0] !== fn)
|
|
4290
|
+
return undefined;
|
|
4291
|
+
if (!isPropertyAccessExpression(call.expression) || call.expression.name.text !== 'sort') {
|
|
4292
|
+
return undefined;
|
|
4293
|
+
}
|
|
4294
|
+
const arrayType = inferExpressionType(call.expression.expression);
|
|
4295
|
+
return arrayType?.startsWith('[]') ? arrayType.slice(2) : undefined;
|
|
4296
|
+
}
|
|
4297
|
+
function isRegexReplacerCallback(fn) {
|
|
4298
|
+
if (!isArrowFunction(fn) && !isFunctionExpression(fn))
|
|
4299
|
+
return false;
|
|
4300
|
+
const call = fn.parent;
|
|
4301
|
+
return !!call && isRegexReplaceCall(call) && call.arguments[1] === fn;
|
|
4302
|
+
}
|
|
4303
|
+
// JS replacement patterns → Go: $& (whole match) → ${0}, $1 → ${1}
|
|
4304
|
+
function jsReplacementToGo(replacement) {
|
|
4305
|
+
if (!isStringLiteral(replacement) && !isNoSubstitutionTemplateLiteral(replacement)) {
|
|
4306
|
+
return visit(replacement);
|
|
4307
|
+
}
|
|
4308
|
+
const text = replacement.text;
|
|
4309
|
+
let goPattern = '';
|
|
4310
|
+
for (let i = 0; i < text.length; i++) {
|
|
4311
|
+
const ch = text[i];
|
|
4312
|
+
const next = i + 1 < text.length ? text[i + 1] : '';
|
|
4313
|
+
if (ch !== '$') {
|
|
4314
|
+
goPattern += ch;
|
|
4315
|
+
}
|
|
4316
|
+
else if (next === '$') {
|
|
4317
|
+
goPattern += '$$';
|
|
4318
|
+
i++;
|
|
4319
|
+
}
|
|
4320
|
+
else if (next === '&') {
|
|
4321
|
+
goPattern += '${0}';
|
|
4322
|
+
i++;
|
|
4323
|
+
}
|
|
4324
|
+
else if (next >= '0' && next <= '9') {
|
|
4325
|
+
let digits = next;
|
|
4326
|
+
i++;
|
|
4327
|
+
while (i + 1 < text.length && text[i + 1] >= '0' && text[i + 1] <= '9') {
|
|
4328
|
+
digits += text[i + 1];
|
|
4329
|
+
i++;
|
|
4330
|
+
}
|
|
4331
|
+
goPattern += '${' + digits + '}';
|
|
4332
|
+
}
|
|
4333
|
+
else {
|
|
4334
|
+
goPattern += '$$';
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
return toGoStringLiteral(goPattern);
|
|
4338
|
+
}
|
|
4339
|
+
// str.replace(/re/g, x) → re.ReplaceAllString; without g only the first match is
|
|
4340
|
+
// replaced; a function replacer receives (match, ...groups)
|
|
4341
|
+
function visitRegexReplace(node) {
|
|
4342
|
+
if (!isRegexReplaceCall(node))
|
|
4343
|
+
return undefined;
|
|
4344
|
+
const [pattern, replacement] = node.arguments;
|
|
4345
|
+
const isGlobal = node.expression.name.text === 'replaceAll' ||
|
|
4346
|
+
(isRegularExpressionLiteral(pattern) &&
|
|
4347
|
+
pattern.text.substring(pattern.text.lastIndexOf('/') + 1).includes('g'));
|
|
4348
|
+
const target = toGoValueOfType(node.expression.expression, 'string');
|
|
4349
|
+
const re = visit(pattern);
|
|
4350
|
+
importedPackages.add('regexp');
|
|
4351
|
+
if (isArrowFunction(replacement) || isFunctionExpression(replacement)) {
|
|
4352
|
+
useHelper('regexReplaceFunc');
|
|
4353
|
+
const callback = visit(replacement);
|
|
4354
|
+
const groupArgs = (replacement.parameters ?? [])
|
|
4355
|
+
.map((_p, index) => `TnGroup(__m, ${index})`)
|
|
4356
|
+
.join(', ');
|
|
4357
|
+
return `TnRegexReplaceFunc(${re}, ${target}, func(__m []string) string { return (${callback})(${groupArgs}) }, ${isGlobal})`;
|
|
4358
|
+
}
|
|
4359
|
+
const goReplacement = jsReplacementToGo(replacement);
|
|
4360
|
+
if (isGlobal)
|
|
4361
|
+
return `${re}.ReplaceAllString(${target}, ${goReplacement})`;
|
|
4362
|
+
useHelper('regexReplaceFirst');
|
|
4363
|
+
return `TnRegexReplaceFirst(${re}, ${target}, ${goReplacement})`;
|
|
4364
|
+
}
|
|
2293
4365
|
function jsRegexFlagsToGo(flags) {
|
|
2294
4366
|
let goFlags = '';
|
|
2295
4367
|
if (flags.includes('i'))
|
|
@@ -2336,7 +4408,9 @@ function getParameterGoType(param) {
|
|
|
2336
4408
|
}
|
|
2337
4409
|
if (param.type) {
|
|
2338
4410
|
const explicitType = getType(param.type);
|
|
2339
|
-
|
|
4411
|
+
if (explicitType === ':')
|
|
4412
|
+
return 'interface{}';
|
|
4413
|
+
return param.questionToken ? makeNullableType(explicitType) : explicitType;
|
|
2340
4414
|
}
|
|
2341
4415
|
if (param.initializer) {
|
|
2342
4416
|
const inferredType = inferExpressionType(param.initializer);
|
|
@@ -2344,29 +4418,174 @@ function getParameterGoType(param) {
|
|
|
2344
4418
|
return inferredType;
|
|
2345
4419
|
}
|
|
2346
4420
|
}
|
|
4421
|
+
if (param.parent && isRegexReplacerCallback(param.parent))
|
|
4422
|
+
return 'string';
|
|
4423
|
+
const sortElementType = param.parent ? getSortComparatorElementType(param.parent) : undefined;
|
|
4424
|
+
if (sortElementType)
|
|
4425
|
+
return sortElementType;
|
|
4426
|
+
const contextualFn = param.parent ? getContextualFunctionType(param.parent) : undefined;
|
|
4427
|
+
if (contextualFn) {
|
|
4428
|
+
const index = (param.parent.parameters ?? []).indexOf(param);
|
|
4429
|
+
const contextualParamType = contextualFn.parameters?.[index]?.type;
|
|
4430
|
+
if (contextualParamType)
|
|
4431
|
+
return getType(contextualParamType);
|
|
4432
|
+
}
|
|
2347
4433
|
return 'interface{}';
|
|
2348
4434
|
}
|
|
4435
|
+
// Parameters are variables of the function body: record their types (replacing
|
|
4436
|
+
// whatever an earlier variable of the same name had)
|
|
4437
|
+
function registerParameterType(param) {
|
|
4438
|
+
if (!isIdentifier(param.name))
|
|
4439
|
+
return;
|
|
4440
|
+
const name = param.name.text;
|
|
4441
|
+
const goType = getParameterGoType(param);
|
|
4442
|
+
variableGoTypes.set(name, goType.startsWith('...') ? `[]${goType.slice(3)}` : goType);
|
|
4443
|
+
const category = param.type ? getTypeCategory(param.type) : undefined;
|
|
4444
|
+
if (category)
|
|
4445
|
+
variableTypes.set(name, category);
|
|
4446
|
+
else
|
|
4447
|
+
variableTypes.delete(name);
|
|
4448
|
+
const className = param.type ? getClassNameFromTypeNode(param.type) : undefined;
|
|
4449
|
+
if (className)
|
|
4450
|
+
variableClassNames.set(name, className);
|
|
4451
|
+
else
|
|
4452
|
+
variableClassNames.delete(name);
|
|
4453
|
+
narrowedVariables.delete(name);
|
|
4454
|
+
if (param.type)
|
|
4455
|
+
variableTypeNodes.set(name, param.type);
|
|
4456
|
+
else
|
|
4457
|
+
variableTypeNodes.delete(name);
|
|
4458
|
+
}
|
|
4459
|
+
// Constructor parameter properties: constructor(private x: T) declares field x
|
|
4460
|
+
function getParameterProperties(classNode) {
|
|
4461
|
+
const ctor = (classNode.members ?? []).find((m) => isConstructorDeclaration(m));
|
|
4462
|
+
return (ctor?.parameters ?? []).filter((p) => isIdentifier(p.name) && (p.modifiers ?? []).length > 0);
|
|
4463
|
+
}
|
|
4464
|
+
// Argument types of map.get/has/set/delete, set.add/has/delete, array.push
|
|
4465
|
+
function getCollectionArgumentTypes(node) {
|
|
4466
|
+
if (!isPropertyAccessExpression(node.expression))
|
|
4467
|
+
return undefined;
|
|
4468
|
+
const method = node.expression.name.text;
|
|
4469
|
+
const receiverType = inferExpressionType(node.expression.expression);
|
|
4470
|
+
if (receiverType?.startsWith('*TnMap[')) {
|
|
4471
|
+
const keyType = extractMapKeyType(receiverType);
|
|
4472
|
+
const valueType = extractMapValueType(receiverType);
|
|
4473
|
+
if (valueType === 'struct{}' && ['add', 'has', 'delete'].includes(method))
|
|
4474
|
+
return [keyType];
|
|
4475
|
+
if (['get', 'has', 'delete'].includes(method))
|
|
4476
|
+
return [keyType];
|
|
4477
|
+
if (method === 'set')
|
|
4478
|
+
return [keyType, valueType];
|
|
4479
|
+
}
|
|
4480
|
+
if (receiverType === '*regexp.Regexp' && (method === 'test' || method === 'exec'))
|
|
4481
|
+
return ['string'];
|
|
4482
|
+
if (receiverType?.startsWith('[]') && method === 'push') {
|
|
4483
|
+
return (node.arguments ?? []).map(() => receiverType.slice(2));
|
|
4484
|
+
}
|
|
4485
|
+
return undefined;
|
|
4486
|
+
}
|
|
4487
|
+
// TS lets a function literal omit trailing parameters of its contextual type;
|
|
4488
|
+
// Go func types must match exactly, so the omitted ones are added as unused `_`
|
|
4489
|
+
// Arguments of a call; for calls to declared functions, values are converted to
|
|
4490
|
+
// the parameter types (nullable primitives boxed) and omitted optional
|
|
4491
|
+
// parameters are passed as zero values (nil for pointers)
|
|
4492
|
+
function visitCallArguments(node) {
|
|
4493
|
+
const args = node.arguments ?? [];
|
|
4494
|
+
const collectionTypes = getCollectionArgumentTypes(node);
|
|
4495
|
+
if (collectionTypes)
|
|
4496
|
+
return args.map((arg, index) => toGoValueOfType(arg, collectionTypes[index]));
|
|
4497
|
+
let fn = isIdentifier(node.expression) ? declaredFunctions.get(node.expression.text) : undefined;
|
|
4498
|
+
if (!fn && isIdentifier(node.expression)) {
|
|
4499
|
+
const fnType = resolveTypeNode(variableTypeNodes.get(node.expression.text));
|
|
4500
|
+
if (isFunctionTypeNode(fnType))
|
|
4501
|
+
fn = fnType;
|
|
4502
|
+
}
|
|
4503
|
+
if (!fn)
|
|
4504
|
+
return args.map((a) => visit(a));
|
|
4505
|
+
const params = fn.parameters ?? [];
|
|
4506
|
+
const result = args.map((arg, index) => {
|
|
4507
|
+
const param = params[index];
|
|
4508
|
+
return param && !param.dotDotDotToken ? toGoValueOfType(arg, getParameterGoType(param)) : visit(arg);
|
|
4509
|
+
});
|
|
4510
|
+
for (let i = args.length; i < params.length; i++) {
|
|
4511
|
+
const param = params[i];
|
|
4512
|
+
if (!param.questionToken || param.initializer || param.dotDotDotToken)
|
|
4513
|
+
break;
|
|
4514
|
+
result.push(`*new(${getParameterGoType(param)})`);
|
|
4515
|
+
}
|
|
4516
|
+
return result;
|
|
4517
|
+
}
|
|
4518
|
+
function withContextualParameters(fn, info) {
|
|
4519
|
+
const contextualFn = getContextualFunctionType(fn);
|
|
4520
|
+
const declaredCount = (fn.parameters ?? []).length;
|
|
4521
|
+
const contextualParams = contextualFn?.parameters ?? [];
|
|
4522
|
+
if (contextualParams.length <= declaredCount || info.signature.includes('...'))
|
|
4523
|
+
return info;
|
|
4524
|
+
const extra = contextualParams
|
|
4525
|
+
.slice(declaredCount)
|
|
4526
|
+
.map((p) => `_ ${p.dotDotDotToken ? '...' : ''}${p.type ? getType(p.type) : 'interface{}'}`);
|
|
4527
|
+
const signature = [info.signature, ...extra].filter((part) => part).join(', ');
|
|
4528
|
+
return { ...info, signature };
|
|
4529
|
+
}
|
|
4530
|
+
// Destructured parameters ({ a, b }: T / [x, y]: T[]) get a temporary name
|
|
4531
|
+
function getParameterName(param, parameters) {
|
|
4532
|
+
if (isIdentifier(param.name))
|
|
4533
|
+
return visit(param.name);
|
|
4534
|
+
return `__param${parameters.indexOf(param)}`;
|
|
4535
|
+
}
|
|
4536
|
+
// Unpacks destructured parameters at the start of the function body
|
|
4537
|
+
function getParameterDestructuring(parameters) {
|
|
4538
|
+
let code = '';
|
|
4539
|
+
for (const param of parameters) {
|
|
4540
|
+
if (isIdentifier(param.name))
|
|
4541
|
+
continue;
|
|
4542
|
+
const source = getParameterName(param, parameters);
|
|
4543
|
+
const paramType = getParameterGoType(param);
|
|
4544
|
+
(param.name.elements ?? []).forEach((el, index) => {
|
|
4545
|
+
if (isOmittedExpression(el) || !isIdentifier(el.name))
|
|
4546
|
+
return;
|
|
4547
|
+
let value;
|
|
4548
|
+
let valueType;
|
|
4549
|
+
if (isObjectBindingPattern(param.name)) {
|
|
4550
|
+
const field = (el.propertyName ?? el.name).text;
|
|
4551
|
+
value = `${source}.${goFieldName(field)}`;
|
|
4552
|
+
valueType =
|
|
4553
|
+
interfacePropertyTypes.get(paramType)?.get(field) ?? getStructFieldGoType(paramType, field);
|
|
4554
|
+
}
|
|
4555
|
+
else {
|
|
4556
|
+
value = `${source}[${index}]`;
|
|
4557
|
+
valueType = paramType.startsWith('[]') ? paramType.slice(2) : undefined;
|
|
4558
|
+
}
|
|
4559
|
+
registerLocalVariable(el.name.text, valueType);
|
|
4560
|
+
code += `${visit(el.name)} := ${value}\n\t\t_ = ${visit(el.name)}\n\t\t`;
|
|
4561
|
+
});
|
|
4562
|
+
}
|
|
4563
|
+
return code;
|
|
4564
|
+
}
|
|
2349
4565
|
function getFunctionParametersInfo(parameters) {
|
|
4566
|
+
for (const param of parameters)
|
|
4567
|
+
registerParameterType(param);
|
|
2350
4568
|
if (parameters.length === 0) {
|
|
2351
4569
|
return { signature: '', prefixBlockContent: '' };
|
|
2352
4570
|
}
|
|
4571
|
+
const destructuring = getParameterDestructuring(parameters);
|
|
2353
4572
|
const firstDefaultIndex = parameters.findIndex((p) => !!p.initializer);
|
|
2354
4573
|
if (firstDefaultIndex === -1) {
|
|
2355
4574
|
return {
|
|
2356
|
-
signature: parameters.map((p) => `${
|
|
2357
|
-
prefixBlockContent:
|
|
4575
|
+
signature: parameters.map((p) => `${getParameterName(p, parameters)} ${getParameterGoType(p)}`).join(', '),
|
|
4576
|
+
prefixBlockContent: destructuring
|
|
2358
4577
|
};
|
|
2359
4578
|
}
|
|
2360
4579
|
const hasRequiredAfterDefault = parameters.slice(firstDefaultIndex).some((p) => !p.initializer);
|
|
2361
4580
|
if (hasRequiredAfterDefault) {
|
|
2362
4581
|
return {
|
|
2363
|
-
signature: parameters.map((p) => `${
|
|
2364
|
-
prefixBlockContent:
|
|
4582
|
+
signature: parameters.map((p) => `${getParameterName(p, parameters)} ${getParameterGoType(p)}`).join(', '),
|
|
4583
|
+
prefixBlockContent: destructuring
|
|
2365
4584
|
};
|
|
2366
4585
|
}
|
|
2367
4586
|
const requiredParams = parameters.slice(0, firstDefaultIndex);
|
|
2368
4587
|
const defaultedParams = parameters.slice(firstDefaultIndex);
|
|
2369
|
-
const signatureParts = requiredParams.map((p) => `${
|
|
4588
|
+
const signatureParts = requiredParams.map((p) => `${getParameterName(p, parameters)} ${getParameterGoType(p)}`);
|
|
2370
4589
|
signatureParts.push('__defaultArgs ...interface{}');
|
|
2371
4590
|
const prefixBlockContent = defaultedParams
|
|
2372
4591
|
.map((param, index) => {
|
|
@@ -2381,7 +4600,7 @@ function getFunctionParametersInfo(parameters) {
|
|
|
2381
4600
|
.join('');
|
|
2382
4601
|
return {
|
|
2383
4602
|
signature: signatureParts.join(', '),
|
|
2384
|
-
prefixBlockContent
|
|
4603
|
+
prefixBlockContent: prefixBlockContent + destructuring
|
|
2385
4604
|
};
|
|
2386
4605
|
}
|
|
2387
4606
|
function getSafeName(name) {
|
|
@@ -2444,7 +4663,9 @@ function visitNewPromise(node) {
|
|
|
2444
4663
|
return `func() chan ${channelType} {\n\t\tch := make(chan ${channelType})\n\t\tgo func() ${body.trimEnd()}()\n\t\treturn ch;\n\t}()`;
|
|
2445
4664
|
}
|
|
2446
4665
|
function extractMapValueType(mapType) {
|
|
2447
|
-
|
|
4666
|
+
const args = splitMapTypeArgs(mapType);
|
|
4667
|
+
if (args)
|
|
4668
|
+
return args[1];
|
|
2448
4669
|
if (!mapType.startsWith('map['))
|
|
2449
4670
|
return 'interface{}';
|
|
2450
4671
|
let depth = 0;
|
|
@@ -2459,43 +4680,43 @@ function extractMapValueType(mapType) {
|
|
|
2459
4680
|
}
|
|
2460
4681
|
return 'interface{}';
|
|
2461
4682
|
}
|
|
4683
|
+
// Type arguments of new Map/Set: explicit, else from the declared/contextual type
|
|
4684
|
+
function getCollectionTypeArguments(node) {
|
|
4685
|
+
if ((node.typeArguments ?? []).length > 0)
|
|
4686
|
+
return node.typeArguments;
|
|
4687
|
+
const contextual = resolveTypeNode(getContextualTypeNode(node));
|
|
4688
|
+
return isTypeReferenceNode(contextual) ? (contextual.typeArguments ?? []) : [];
|
|
4689
|
+
}
|
|
2462
4690
|
function visitNewMap(node) {
|
|
2463
4691
|
let keyType = 'interface{}';
|
|
2464
4692
|
let valueType = 'interface{}';
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
const args =
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
.filter((el) => isArrayLiteralExpression(el) && el.elements.length >= 2)
|
|
2478
|
-
.map((el) => {
|
|
2479
|
-
const pair = el;
|
|
2480
|
-
return `${tmp}[${visit(pair.elements[0])}] = ${visit(pair.elements[1])}`;
|
|
2481
|
-
})
|
|
2482
|
-
.join('; ');
|
|
2483
|
-
return `func() ${mapType} { ${tmp} := make(${mapType}); ${entries}; return ${tmp} }()`;
|
|
4693
|
+
const typeArguments = getCollectionTypeArguments(node);
|
|
4694
|
+
if (typeArguments.length === 2) {
|
|
4695
|
+
keyType = getType(typeArguments[0]);
|
|
4696
|
+
valueType = getType(typeArguments[1]);
|
|
4697
|
+
}
|
|
4698
|
+
const args = node.arguments ?? [];
|
|
4699
|
+
const entries = args.length > 0 && isArrayLiteralExpression(args[0])
|
|
4700
|
+
? args[0].elements
|
|
4701
|
+
.filter((el) => isArrayLiteralExpression(el) && el.elements.length >= 2)
|
|
4702
|
+
.map((pair) => `${visit(pair.elements[0])}, ${visit(pair.elements[1])}`)
|
|
4703
|
+
: [];
|
|
4704
|
+
return orderedMapLiteral(keyType, valueType, entries);
|
|
2484
4705
|
}
|
|
2485
4706
|
function visitNewSet(node) {
|
|
2486
4707
|
let elementType = 'interface{}';
|
|
2487
|
-
|
|
2488
|
-
|
|
4708
|
+
const typeArguments = getCollectionTypeArguments(node);
|
|
4709
|
+
if (typeArguments.length === 1) {
|
|
4710
|
+
elementType = getType(typeArguments[0]);
|
|
2489
4711
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
if (!args || args.length === 0 || !isArrayLiteralExpression(args[0])) {
|
|
2493
|
-
return `make(${setType})`;
|
|
4712
|
+
else if (node.arguments?.length && isArrayLiteralExpression(node.arguments[0])) {
|
|
4713
|
+
elementType = getArrayLiteralElementType(node.arguments[0]);
|
|
2494
4714
|
}
|
|
2495
|
-
const
|
|
2496
|
-
const
|
|
2497
|
-
|
|
2498
|
-
|
|
4715
|
+
const args = node.arguments ?? [];
|
|
4716
|
+
const values = args.length > 0 && isArrayLiteralExpression(args[0])
|
|
4717
|
+
? args[0].elements.map((el) => `${visit(el)}, struct{}{}`)
|
|
4718
|
+
: [];
|
|
4719
|
+
return orderedMapLiteral(elementType, 'struct{}', values);
|
|
2499
4720
|
}
|
|
2500
4721
|
function specifierToGoFileName(specifier) {
|
|
2501
4722
|
const segments = specifier.split(/[/\\]/);
|
|
@@ -2530,7 +4751,7 @@ function includeLocalImport(code, dir, goFileName) {
|
|
|
2530
4751
|
}
|
|
2531
4752
|
if (!goFileName) {
|
|
2532
4753
|
// Inline mode (npm packages): pull declarations into the current transpilation context
|
|
2533
|
-
const sf =
|
|
4754
|
+
const sf = parseSource(code);
|
|
2534
4755
|
for (const stmt of sf.statements) {
|
|
2535
4756
|
visit(stmt, { addFunctionOutside: true });
|
|
2536
4757
|
}
|
|
@@ -2545,13 +4766,16 @@ function includeLocalImport(code, dir, goFileName) {
|
|
|
2545
4766
|
outsideNodes = [];
|
|
2546
4767
|
importedPackages.clear();
|
|
2547
4768
|
helperProvidedPackages.clear();
|
|
2548
|
-
const sf =
|
|
4769
|
+
const sf = parseSource(code);
|
|
2549
4770
|
const inlineLines = [];
|
|
4771
|
+
const prevEmittingModuleFile = emittingModuleFile;
|
|
4772
|
+
emittingModuleFile = true;
|
|
2550
4773
|
for (const stmt of sf.statements) {
|
|
2551
4774
|
const result = visit(stmt, { addFunctionOutside: true });
|
|
2552
4775
|
if (result.trim())
|
|
2553
4776
|
inlineLines.push(result);
|
|
2554
4777
|
}
|
|
4778
|
+
emittingModuleFile = prevEmittingModuleFile;
|
|
2555
4779
|
const fileOutside = outsideNodes.map((n) => visit(n, { isOutside: true })).join('\n');
|
|
2556
4780
|
const fileInline = inlineLines.join('\n');
|
|
2557
4781
|
// Computed after all visits: function bodies (outside nodes) register
|
|
@@ -2565,8 +4789,9 @@ function includeLocalImport(code, dir, goFileName) {
|
|
|
2565
4789
|
if (!helperProvidedPackages.has(pkg))
|
|
2566
4790
|
return true;
|
|
2567
4791
|
const name = pkg.split('/').pop();
|
|
2568
|
-
return fileCode.includes(`${name}.`);
|
|
4792
|
+
return stripGoStrings(fileCode).includes(`${name}.`);
|
|
2569
4793
|
})
|
|
4794
|
+
.sort()
|
|
2570
4795
|
.map((pkg) => `import "${pkg}"`)
|
|
2571
4796
|
.join('\n');
|
|
2572
4797
|
const parts = ['package main'];
|
|
@@ -2587,6 +4812,10 @@ function includeLocalImport(code, dir, goFileName) {
|
|
|
2587
4812
|
importedPackages.add(p);
|
|
2588
4813
|
currentFileDir = prevDir;
|
|
2589
4814
|
}
|
|
4815
|
+
// Go source without its string literals (for detecting real package use)
|
|
4816
|
+
function stripGoStrings(code) {
|
|
4817
|
+
return code.replace(/"(?:[^"\\\n]|\\.)*"|`[^`]*`/g, '""');
|
|
4818
|
+
}
|
|
2590
4819
|
function registerGoPackageAliases(node, goPkg) {
|
|
2591
4820
|
const pkgName = goPkg.split('/').pop();
|
|
2592
4821
|
if (!node.importClause)
|
|
@@ -2710,6 +4939,10 @@ const nodeModuleMappings = {
|
|
|
2710
4939
|
rmSync: (args) => {
|
|
2711
4940
|
useHelper('removeAll');
|
|
2712
4941
|
return `TnRemoveAll(${args[0]})`;
|
|
4942
|
+
},
|
|
4943
|
+
unlinkSync: (args) => {
|
|
4944
|
+
useHelper('removeAll');
|
|
4945
|
+
return `TnRemoveAll(${args[0]})`;
|
|
2713
4946
|
}
|
|
2714
4947
|
}
|
|
2715
4948
|
},
|
|
@@ -2760,15 +4993,15 @@ const nodeModuleMappings = {
|
|
|
2760
4993
|
spawnSync: (args) => {
|
|
2761
4994
|
useHelper('exec');
|
|
2762
4995
|
useHelper('runInherit');
|
|
2763
|
-
if (args[2]?.includes('inherit')) {
|
|
2764
|
-
return `TnRunInherit(${args[0]})`;
|
|
2765
|
-
}
|
|
2766
4996
|
// Untyped array literals visit as `[] {…}` — give them the []string
|
|
2767
4997
|
// type the variadic helper needs (`[]string{…}...` is valid Go spread)
|
|
2768
4998
|
const arrArg = args[1] ?? '[]string{}';
|
|
2769
4999
|
const typedArg = arrArg.startsWith('[]') && !arrArg.startsWith('[]string')
|
|
2770
|
-
?
|
|
5000
|
+
? arrArg.replace(/^\[\]\S* \{/, '[]string{')
|
|
2771
5001
|
: arrArg;
|
|
5002
|
+
if (args[2]?.includes('inherit')) {
|
|
5003
|
+
return `TnRunInherit(${args[0]}, ${typedArg}...)`;
|
|
5004
|
+
}
|
|
2772
5005
|
if (args[2]?.includes('input')) {
|
|
2773
5006
|
// {input: expr, ...} → run with expr as stdin
|
|
2774
5007
|
useHelper('execInput');
|
|
@@ -2792,6 +5025,12 @@ const nodeModuleMappings = {
|
|
|
2792
5025
|
};
|
|
2793
5026
|
// Go packages required by each helper, registered when the helper is used.
|
|
2794
5027
|
const helperPackages = {
|
|
5028
|
+
regexReplaceFirst: ['regexp'],
|
|
5029
|
+
orderedMap: ['iter', 'encoding/json', 'bytes', 'fmt'],
|
|
5030
|
+
jsonStringify: ['encoding/json', 'bytes', 'strings'],
|
|
5031
|
+
typeOf: ['reflect'],
|
|
5032
|
+
dynamic: ['math', 'strings', 'fmt', 'sort', 'reflect', 'strconv', 'regexp'],
|
|
5033
|
+
regexReplaceFunc: ['regexp'],
|
|
2795
5034
|
readFile: ['os'],
|
|
2796
5035
|
writeFile: ['os'],
|
|
2797
5036
|
appendFile: ['os'],
|
|
@@ -2813,6 +5052,535 @@ const helperPackages = {
|
|
|
2813
5052
|
// Field names are intentionally lowercase: every generated file is `package main`,
|
|
2814
5053
|
// so `result.stdout` in emitted code resolves to the struct field directly.
|
|
2815
5054
|
const goHelpers = {
|
|
5055
|
+
dynamic: `// Dynamic (any) values: JSON-like objects are map[string]interface{},
|
|
5056
|
+
// arrays []interface{}, numbers float64
|
|
5057
|
+
func TnGet(obj interface{}, key string) interface{} {
|
|
5058
|
+
switch o := obj.(type) {
|
|
5059
|
+
case map[string]interface{}:
|
|
5060
|
+
return o[key]
|
|
5061
|
+
case []interface{}:
|
|
5062
|
+
if key == "length" {
|
|
5063
|
+
return float64(len(o))
|
|
5064
|
+
}
|
|
5065
|
+
case string:
|
|
5066
|
+
if key == "length" {
|
|
5067
|
+
return float64(len(o))
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
return nil
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
func TnSet(obj interface{}, key string, value interface{}) {
|
|
5074
|
+
if o, ok := obj.(map[string]interface{}); ok {
|
|
5075
|
+
o[key] = value
|
|
5076
|
+
return
|
|
5077
|
+
}
|
|
5078
|
+
panic("TnSet: cannot set property " + key)
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
func TnIndex(obj interface{}, key interface{}) interface{} {
|
|
5082
|
+
switch o := obj.(type) {
|
|
5083
|
+
case map[string]interface{}:
|
|
5084
|
+
if k, ok := key.(string); ok {
|
|
5085
|
+
return o[k]
|
|
5086
|
+
}
|
|
5087
|
+
case []interface{}:
|
|
5088
|
+
if i, ok := key.(float64); ok && i >= 0 && int(i) < len(o) {
|
|
5089
|
+
return o[int(i)]
|
|
5090
|
+
}
|
|
5091
|
+
case string:
|
|
5092
|
+
if i, ok := key.(float64); ok && i >= 0 && int(i) < len(o) {
|
|
5093
|
+
return string(o[int(i)])
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
return nil
|
|
5097
|
+
}
|
|
5098
|
+
|
|
5099
|
+
// JS ===: objects and arrays compare by identity, other values by value
|
|
5100
|
+
func TnSame(a interface{}, b interface{}) bool {
|
|
5101
|
+
if a == nil || b == nil {
|
|
5102
|
+
return a == nil && b == nil
|
|
5103
|
+
}
|
|
5104
|
+
va, vb := reflect.ValueOf(a), reflect.ValueOf(b)
|
|
5105
|
+
switch va.Kind() {
|
|
5106
|
+
case reflect.Map, reflect.Slice, reflect.Func, reflect.Pointer:
|
|
5107
|
+
return va.Kind() == vb.Kind() && va.Type() == vb.Type() && va.Pointer() == vb.Pointer() &&
|
|
5108
|
+
(va.Kind() != reflect.Slice || va.Len() == vb.Len())
|
|
5109
|
+
}
|
|
5110
|
+
if !va.Type().Comparable() || !vb.Type().Comparable() {
|
|
5111
|
+
return false
|
|
5112
|
+
}
|
|
5113
|
+
return a == b
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
func TnAt[T any](s []T, i float64) T {
|
|
5117
|
+
if i < 0 || int(i) >= len(s) {
|
|
5118
|
+
var zero T
|
|
5119
|
+
return zero
|
|
5120
|
+
}
|
|
5121
|
+
return s[int(i)]
|
|
5122
|
+
}
|
|
5123
|
+
|
|
5124
|
+
func TnCharAt(s string, i float64) string {
|
|
5125
|
+
if i < 0 || int(i) >= len(s) {
|
|
5126
|
+
return ""
|
|
5127
|
+
}
|
|
5128
|
+
return string(s[int(i)])
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
func TnLength(v interface{}) float64 {
|
|
5132
|
+
switch t := v.(type) {
|
|
5133
|
+
case string:
|
|
5134
|
+
return float64(len(t))
|
|
5135
|
+
case []interface{}:
|
|
5136
|
+
return float64(len(t))
|
|
5137
|
+
}
|
|
5138
|
+
return 0
|
|
5139
|
+
}
|
|
5140
|
+
|
|
5141
|
+
// A value as JS prints it in a template literal (nil pointers are "undefined")
|
|
5142
|
+
func TnFormat(v interface{}) string {
|
|
5143
|
+
rv := reflect.ValueOf(v)
|
|
5144
|
+
if !rv.IsValid() || (rv.Kind() == reflect.Pointer && rv.IsNil()) {
|
|
5145
|
+
return "undefined"
|
|
5146
|
+
}
|
|
5147
|
+
if rv.Kind() == reflect.Pointer {
|
|
5148
|
+
return fmt.Sprintf("%v", rv.Elem().Interface())
|
|
5149
|
+
}
|
|
5150
|
+
return fmt.Sprintf("%v", v)
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
// Object spread: copies the properties of src into dst
|
|
5154
|
+
func TnAssign(dst map[string]interface{}, src interface{}) {
|
|
5155
|
+
if o, ok := src.(map[string]interface{}); ok {
|
|
5156
|
+
for k, v := range o {
|
|
5157
|
+
dst[k] = v
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
// A number as JS prints it
|
|
5163
|
+
func TnNumStr(f float64) string {
|
|
5164
|
+
switch {
|
|
5165
|
+
case math.IsNaN(f):
|
|
5166
|
+
return "NaN"
|
|
5167
|
+
case math.IsInf(f, 1):
|
|
5168
|
+
return "Infinity"
|
|
5169
|
+
case math.IsInf(f, -1):
|
|
5170
|
+
return "-Infinity"
|
|
5171
|
+
case f == math.Trunc(f) && math.Abs(f) < 1e21:
|
|
5172
|
+
return strconv.FormatFloat(f, 'f', -1, 64)
|
|
5173
|
+
}
|
|
5174
|
+
return strconv.FormatFloat(f, 'g', -1, 64)
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5177
|
+
// Number(x) like JS: "" → 0, invalid → NaN, booleans → 0/1
|
|
5178
|
+
func TnNumber(v interface{}) float64 {
|
|
5179
|
+
switch t := v.(type) {
|
|
5180
|
+
case float64:
|
|
5181
|
+
return t
|
|
5182
|
+
case bool:
|
|
5183
|
+
if t {
|
|
5184
|
+
return 1
|
|
5185
|
+
}
|
|
5186
|
+
return 0
|
|
5187
|
+
case string:
|
|
5188
|
+
s := strings.TrimSpace(t)
|
|
5189
|
+
if s == "" {
|
|
5190
|
+
return 0
|
|
5191
|
+
}
|
|
5192
|
+
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|
5193
|
+
return f
|
|
5194
|
+
}
|
|
5195
|
+
case nil:
|
|
5196
|
+
return 0
|
|
5197
|
+
}
|
|
5198
|
+
return math.NaN()
|
|
5199
|
+
}
|
|
5200
|
+
|
|
5201
|
+
var tnLeadingFloat = regexp.MustCompile("^[+-]?(Infinity|[0-9]+[.]?[0-9]*([eE][+-]?[0-9]+)?|[.][0-9]+([eE][+-]?[0-9]+)?)")
|
|
5202
|
+
|
|
5203
|
+
// parseFloat: the leading number of the string, NaN if none
|
|
5204
|
+
func TnParseFloat(s string) float64 {
|
|
5205
|
+
m := tnLeadingFloat.FindString(strings.TrimSpace(s))
|
|
5206
|
+
if m == "" {
|
|
5207
|
+
return math.NaN()
|
|
5208
|
+
}
|
|
5209
|
+
if strings.HasSuffix(m, "Infinity") {
|
|
5210
|
+
if strings.HasPrefix(m, "-") {
|
|
5211
|
+
return math.Inf(-1)
|
|
5212
|
+
}
|
|
5213
|
+
return math.Inf(1)
|
|
5214
|
+
}
|
|
5215
|
+
f, _ := strconv.ParseFloat(m, 64)
|
|
5216
|
+
return f
|
|
5217
|
+
}
|
|
5218
|
+
|
|
5219
|
+
// parseInt: the leading integer in the given radix, NaN if none
|
|
5220
|
+
func TnParseInt(s string, radix float64) float64 {
|
|
5221
|
+
s = strings.TrimSpace(s)
|
|
5222
|
+
base := int(radix)
|
|
5223
|
+
sign := 1.0
|
|
5224
|
+
if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "+") {
|
|
5225
|
+
if s[0] == '-' {
|
|
5226
|
+
sign = -1
|
|
5227
|
+
}
|
|
5228
|
+
s = s[1:]
|
|
5229
|
+
}
|
|
5230
|
+
if (base == 16 || base == 0) && (strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X")) {
|
|
5231
|
+
s, base = s[2:], 16
|
|
5232
|
+
}
|
|
5233
|
+
if base == 0 {
|
|
5234
|
+
base = 10
|
|
5235
|
+
}
|
|
5236
|
+
end := 0
|
|
5237
|
+
for end < len(s) {
|
|
5238
|
+
c := s[end]
|
|
5239
|
+
d := 99
|
|
5240
|
+
switch {
|
|
5241
|
+
case c >= '0' && c <= '9':
|
|
5242
|
+
d = int(c - '0')
|
|
5243
|
+
case c >= 'a' && c <= 'z':
|
|
5244
|
+
d = int(c-'a') + 10
|
|
5245
|
+
case c >= 'A' && c <= 'Z':
|
|
5246
|
+
d = int(c-'A') + 10
|
|
5247
|
+
}
|
|
5248
|
+
if d >= base {
|
|
5249
|
+
break
|
|
5250
|
+
}
|
|
5251
|
+
end++
|
|
5252
|
+
}
|
|
5253
|
+
if end == 0 {
|
|
5254
|
+
return math.NaN()
|
|
5255
|
+
}
|
|
5256
|
+
v, _ := strconv.ParseInt(s[:end], base, 64)
|
|
5257
|
+
return sign * float64(v)
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5260
|
+
// JS ToInt32 / ToUint32 (wrapping modulo 2^32; NaN and Infinity → 0)
|
|
5261
|
+
func TnInt32(f float64) int32 {
|
|
5262
|
+
if math.IsNaN(f) || math.IsInf(f, 0) {
|
|
5263
|
+
return 0
|
|
5264
|
+
}
|
|
5265
|
+
return int32(int64(math.Trunc(math.Mod(f, 4294967296))))
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
func TnUint32(f float64) uint32 { return uint32(TnInt32(f)) }
|
|
5269
|
+
|
|
5270
|
+
func TnDelete(obj interface{}, key interface{}) bool {
|
|
5271
|
+
o, ok := obj.(map[string]interface{})
|
|
5272
|
+
k, isString := key.(string)
|
|
5273
|
+
if !ok || !isString {
|
|
5274
|
+
return false
|
|
5275
|
+
}
|
|
5276
|
+
delete(o, k)
|
|
5277
|
+
return true
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
func TnHas(obj interface{}, key interface{}) bool {
|
|
5281
|
+
if o, ok := obj.(map[string]interface{}); ok {
|
|
5282
|
+
if k, ok := key.(string); ok {
|
|
5283
|
+
_, found := o[k]
|
|
5284
|
+
return found
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
return false
|
|
5288
|
+
}
|
|
5289
|
+
|
|
5290
|
+
func TnKeys(obj interface{}) []string {
|
|
5291
|
+
o, _ := obj.(map[string]interface{})
|
|
5292
|
+
keys := make([]string, 0, len(o))
|
|
5293
|
+
for k := range o {
|
|
5294
|
+
keys = append(keys, k)
|
|
5295
|
+
}
|
|
5296
|
+
sort.Strings(keys)
|
|
5297
|
+
return keys
|
|
5298
|
+
}
|
|
5299
|
+
|
|
5300
|
+
func TnIncludes(v interface{}, x interface{}) bool {
|
|
5301
|
+
return TnIndexOf(v, x) >= 0
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5304
|
+
func TnIndexOf(v interface{}, x interface{}) float64 {
|
|
5305
|
+
switch t := v.(type) {
|
|
5306
|
+
case string:
|
|
5307
|
+
if s, ok := x.(string); ok {
|
|
5308
|
+
return float64(strings.Index(t, s))
|
|
5309
|
+
}
|
|
5310
|
+
case []interface{}:
|
|
5311
|
+
for i, item := range t {
|
|
5312
|
+
if TnSame(item, x) {
|
|
5313
|
+
return float64(i)
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
return -1
|
|
5318
|
+
}
|
|
5319
|
+
|
|
5320
|
+
func TnSlice(v interface{}, bounds ...float64) interface{} {
|
|
5321
|
+
length := 0
|
|
5322
|
+
switch t := v.(type) {
|
|
5323
|
+
case string:
|
|
5324
|
+
length = len(t)
|
|
5325
|
+
case []interface{}:
|
|
5326
|
+
length = len(t)
|
|
5327
|
+
default:
|
|
5328
|
+
return nil
|
|
5329
|
+
}
|
|
5330
|
+
clamp := func(i float64) int {
|
|
5331
|
+
n := int(i)
|
|
5332
|
+
if n < 0 {
|
|
5333
|
+
n += length
|
|
5334
|
+
}
|
|
5335
|
+
return max(0, min(n, length))
|
|
5336
|
+
}
|
|
5337
|
+
start, end := 0, length
|
|
5338
|
+
if len(bounds) > 0 {
|
|
5339
|
+
start = clamp(bounds[0])
|
|
5340
|
+
}
|
|
5341
|
+
if len(bounds) > 1 {
|
|
5342
|
+
end = clamp(bounds[1])
|
|
5343
|
+
}
|
|
5344
|
+
if start > end {
|
|
5345
|
+
start = end
|
|
5346
|
+
}
|
|
5347
|
+
if s, ok := v.(string); ok {
|
|
5348
|
+
return s[start:end]
|
|
5349
|
+
}
|
|
5350
|
+
return v.([]interface{})[start:end]
|
|
5351
|
+
}
|
|
5352
|
+
|
|
5353
|
+
func TnConcat(v interface{}, others ...interface{}) interface{} {
|
|
5354
|
+
if s, ok := v.(string); ok {
|
|
5355
|
+
for _, o := range others {
|
|
5356
|
+
s += fmt.Sprint(o)
|
|
5357
|
+
}
|
|
5358
|
+
return s
|
|
5359
|
+
}
|
|
5360
|
+
result := append([]interface{}{}, TnAs[[]interface{}](v)...)
|
|
5361
|
+
for _, o := range others {
|
|
5362
|
+
if items, ok := o.([]interface{}); ok {
|
|
5363
|
+
result = append(result, items...)
|
|
5364
|
+
} else {
|
|
5365
|
+
result = append(result, o)
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
return result
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
func TnAsPtr[T any](v interface{}) *T {
|
|
5372
|
+
if p, ok := v.(*T); ok {
|
|
5373
|
+
return p
|
|
5374
|
+
}
|
|
5375
|
+
if t, ok := v.(T); ok {
|
|
5376
|
+
return &t
|
|
5377
|
+
}
|
|
5378
|
+
return nil
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
func TnAs[T any](v interface{}) T {
|
|
5382
|
+
if p, ok := v.(*T); ok && p != nil {
|
|
5383
|
+
return *p
|
|
5384
|
+
}
|
|
5385
|
+
t, _ := v.(T)
|
|
5386
|
+
return t
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5389
|
+
func TnTruthy(v interface{}) bool {
|
|
5390
|
+
switch t := v.(type) {
|
|
5391
|
+
case nil:
|
|
5392
|
+
return false
|
|
5393
|
+
case bool:
|
|
5394
|
+
return t
|
|
5395
|
+
case float64:
|
|
5396
|
+
return t != 0 && !math.IsNaN(t)
|
|
5397
|
+
case string:
|
|
5398
|
+
return t != ""
|
|
5399
|
+
}
|
|
5400
|
+
return true
|
|
5401
|
+
}`,
|
|
5402
|
+
typeOf: `func TnTypeOf(v interface{}) string {
|
|
5403
|
+
switch v.(type) {
|
|
5404
|
+
case nil:
|
|
5405
|
+
return "undefined"
|
|
5406
|
+
case string, *string:
|
|
5407
|
+
return "string"
|
|
5408
|
+
case float64, *float64:
|
|
5409
|
+
return "number"
|
|
5410
|
+
case bool, *bool:
|
|
5411
|
+
return "boolean"
|
|
5412
|
+
}
|
|
5413
|
+
if reflect.ValueOf(v).Kind() == reflect.Func {
|
|
5414
|
+
return "function"
|
|
5415
|
+
}
|
|
5416
|
+
return "object"
|
|
5417
|
+
}`,
|
|
5418
|
+
jsonStringify: `func TnJSONStringify(v interface{}, indent string) string {
|
|
5419
|
+
var buf bytes.Buffer
|
|
5420
|
+
enc := json.NewEncoder(&buf)
|
|
5421
|
+
enc.SetEscapeHTML(false)
|
|
5422
|
+
enc.SetIndent("", indent)
|
|
5423
|
+
enc.Encode(v)
|
|
5424
|
+
return strings.TrimSuffix(buf.String(), "\\n")
|
|
5425
|
+
}`,
|
|
5426
|
+
orderedMap: `// TnMap: JS Map/Set/object semantics — keys iterate in insertion order
|
|
5427
|
+
type TnMap[K comparable, V any] struct {
|
|
5428
|
+
keys []K
|
|
5429
|
+
values map[K]V
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5432
|
+
func TnNewMap[K comparable, V any]() *TnMap[K, V] {
|
|
5433
|
+
return &TnMap[K, V]{values: map[K]V{}}
|
|
5434
|
+
}
|
|
5435
|
+
|
|
5436
|
+
func (m *TnMap[K, V]) Set(k K, v V) *TnMap[K, V] {
|
|
5437
|
+
if _, ok := m.values[k]; !ok {
|
|
5438
|
+
m.keys = append(m.keys, k)
|
|
5439
|
+
}
|
|
5440
|
+
m.values[k] = v
|
|
5441
|
+
return m
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
func (m *TnMap[K, V]) Get(k K) V {
|
|
5445
|
+
if m == nil {
|
|
5446
|
+
var zero V
|
|
5447
|
+
return zero
|
|
5448
|
+
}
|
|
5449
|
+
return m.values[k]
|
|
5450
|
+
}
|
|
5451
|
+
|
|
5452
|
+
func (m *TnMap[K, V]) Lookup(k K) (V, bool) {
|
|
5453
|
+
if m == nil {
|
|
5454
|
+
var zero V
|
|
5455
|
+
return zero, false
|
|
5456
|
+
}
|
|
5457
|
+
v, ok := m.values[k]
|
|
5458
|
+
return v, ok
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
func (m *TnMap[K, V]) Has(k K) bool {
|
|
5462
|
+
if m == nil {
|
|
5463
|
+
return false
|
|
5464
|
+
}
|
|
5465
|
+
_, ok := m.values[k]
|
|
5466
|
+
return ok
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
func (m *TnMap[K, V]) Delete(k K) bool {
|
|
5470
|
+
if _, ok := m.values[k]; !ok {
|
|
5471
|
+
return false
|
|
5472
|
+
}
|
|
5473
|
+
delete(m.values, k)
|
|
5474
|
+
for i, key := range m.keys {
|
|
5475
|
+
if key == k {
|
|
5476
|
+
m.keys = append(m.keys[:i], m.keys[i+1:]...)
|
|
5477
|
+
break
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
return true
|
|
5481
|
+
}
|
|
5482
|
+
|
|
5483
|
+
func (m *TnMap[K, V]) Clear() {
|
|
5484
|
+
m.keys = nil
|
|
5485
|
+
m.values = map[K]V{}
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
func (m *TnMap[K, V]) Len() int {
|
|
5489
|
+
if m == nil {
|
|
5490
|
+
return 0
|
|
5491
|
+
}
|
|
5492
|
+
return len(m.keys)
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5495
|
+
func (m *TnMap[K, V]) Keys() []K {
|
|
5496
|
+
if m == nil {
|
|
5497
|
+
return nil
|
|
5498
|
+
}
|
|
5499
|
+
return append([]K{}, m.keys...)
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5502
|
+
func (m *TnMap[K, V]) Values() []V {
|
|
5503
|
+
if m == nil {
|
|
5504
|
+
return nil
|
|
5505
|
+
}
|
|
5506
|
+
values := make([]V, 0, len(m.keys))
|
|
5507
|
+
for _, k := range m.keys {
|
|
5508
|
+
values = append(values, m.values[k])
|
|
5509
|
+
}
|
|
5510
|
+
return values
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
func (m *TnMap[K, V]) Entries() [][]interface{} {
|
|
5514
|
+
entries := make([][]interface{}, 0, m.Len())
|
|
5515
|
+
for _, k := range m.Keys() {
|
|
5516
|
+
entries = append(entries, []interface{}{k, m.values[k]})
|
|
5517
|
+
}
|
|
5518
|
+
return entries
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
func (m *TnMap[K, V]) All() iter.Seq2[K, V] {
|
|
5522
|
+
return func(yield func(K, V) bool) {
|
|
5523
|
+
for _, k := range m.Keys() {
|
|
5524
|
+
if v, ok := m.values[k]; ok && !yield(k, v) {
|
|
5525
|
+
return
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
// JSON objects keep insertion order
|
|
5532
|
+
func (m *TnMap[K, V]) MarshalJSON() ([]byte, error) {
|
|
5533
|
+
var buf bytes.Buffer
|
|
5534
|
+
buf.WriteByte('{')
|
|
5535
|
+
for i, k := range m.keys {
|
|
5536
|
+
if i > 0 {
|
|
5537
|
+
buf.WriteByte(',')
|
|
5538
|
+
}
|
|
5539
|
+
key, _ := json.Marshal(fmt.Sprint(k))
|
|
5540
|
+
value, err := json.Marshal(m.values[k])
|
|
5541
|
+
if err != nil {
|
|
5542
|
+
return nil, err
|
|
5543
|
+
}
|
|
5544
|
+
buf.Write(key)
|
|
5545
|
+
buf.WriteByte(':')
|
|
5546
|
+
buf.Write(value)
|
|
5547
|
+
}
|
|
5548
|
+
buf.WriteByte('}')
|
|
5549
|
+
return buf.Bytes(), nil
|
|
5550
|
+
}`,
|
|
5551
|
+
regexReplaceFirst: `func TnRegexReplaceFirst(re *regexp.Regexp, s string, repl string) string {
|
|
5552
|
+
loc := re.FindStringSubmatchIndex(s)
|
|
5553
|
+
if loc == nil {
|
|
5554
|
+
return s
|
|
5555
|
+
}
|
|
5556
|
+
return s[:loc[0]] + string(re.ExpandString(nil, repl, s, loc)) + s[loc[1]:]
|
|
5557
|
+
}`,
|
|
5558
|
+
regexReplaceFunc: `func TnRegexReplaceFunc(re *regexp.Regexp, s string, fn func([]string) string, all bool) string {
|
|
5559
|
+
limit := 1
|
|
5560
|
+
if all {
|
|
5561
|
+
limit = -1
|
|
5562
|
+
}
|
|
5563
|
+
result := ""
|
|
5564
|
+
last := 0
|
|
5565
|
+
for _, loc := range re.FindAllStringSubmatchIndex(s, limit) {
|
|
5566
|
+
groups := make([]string, len(loc)/2)
|
|
5567
|
+
for i := range groups {
|
|
5568
|
+
if loc[2*i] >= 0 {
|
|
5569
|
+
groups[i] = s[loc[2*i]:loc[2*i+1]]
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
result += s[last:loc[0]] + fn(groups)
|
|
5573
|
+
last = loc[1]
|
|
5574
|
+
}
|
|
5575
|
+
return result + s[last:]
|
|
5576
|
+
}
|
|
5577
|
+
|
|
5578
|
+
func TnGroup(groups []string, i int) string {
|
|
5579
|
+
if i < len(groups) {
|
|
5580
|
+
return groups[i]
|
|
5581
|
+
}
|
|
5582
|
+
return ""
|
|
5583
|
+
}`,
|
|
2816
5584
|
readFile: `func TnReadFile(path string) string {
|
|
2817
5585
|
data, err := os.ReadFile(path)
|
|
2818
5586
|
if err != nil {
|
|
@@ -2953,7 +5721,7 @@ func TnExecShell(command string) tnExecResult {
|
|
|
2953
5721
|
}
|
|
2954
5722
|
return tnExecResult{stdout: stdout.String(), stderr: stderr.String(), status: status}
|
|
2955
5723
|
}`,
|
|
2956
|
-
runInherit: `func TnRunInherit(name string, args ...string)
|
|
5724
|
+
runInherit: `func TnRunInherit(name string, args ...string) tnExecResult {
|
|
2957
5725
|
cmd := exec.Command(name, args...)
|
|
2958
5726
|
cmd.Stdin = os.Stdin
|
|
2959
5727
|
cmd.Stdout = os.Stdout
|
|
@@ -2961,11 +5729,11 @@ func TnExecShell(command string) tnExecResult {
|
|
|
2961
5729
|
err := cmd.Run()
|
|
2962
5730
|
if err != nil {
|
|
2963
5731
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
2964
|
-
return float64(exitErr.ExitCode())
|
|
5732
|
+
return tnExecResult{status: float64(exitErr.ExitCode())}
|
|
2965
5733
|
}
|
|
2966
5734
|
panic(err)
|
|
2967
5735
|
}
|
|
2968
|
-
return
|
|
5736
|
+
return tnExecResult{}
|
|
2969
5737
|
}`,
|
|
2970
5738
|
question: `var tnStdinReader = bufio.NewReader(os.Stdin)
|
|
2971
5739
|
|
|
@@ -2987,10 +5755,41 @@ func TnQuestion(prompt string) string {
|
|
|
2987
5755
|
// so helpers are visible across files and must not be duplicated.
|
|
2988
5756
|
function emitGoHelpers() {
|
|
2989
5757
|
return [...usedHelpers]
|
|
5758
|
+
.sort()
|
|
2990
5759
|
.map((id) => goHelpers[id])
|
|
2991
5760
|
.filter(Boolean)
|
|
2992
5761
|
.join('\n\n');
|
|
2993
5762
|
}
|
|
5763
|
+
// Result types of the mapped Node.js stdlib functions ('' = no result)
|
|
5764
|
+
const NODE_FUNCTION_TYPES = {
|
|
5765
|
+
path: {
|
|
5766
|
+
join: 'string',
|
|
5767
|
+
resolve: 'string',
|
|
5768
|
+
dirname: 'string',
|
|
5769
|
+
basename: 'string',
|
|
5770
|
+
extname: 'string',
|
|
5771
|
+
relative: 'string',
|
|
5772
|
+
normalize: 'string'
|
|
5773
|
+
},
|
|
5774
|
+
fs: {
|
|
5775
|
+
readFileSync: 'string',
|
|
5776
|
+
existsSync: 'bool',
|
|
5777
|
+
readdirSync: '[]string',
|
|
5778
|
+
writeFileSync: '',
|
|
5779
|
+
appendFileSync: '',
|
|
5780
|
+
mkdirSync: '',
|
|
5781
|
+
copyFileSync: '',
|
|
5782
|
+
rmSync: '',
|
|
5783
|
+
unlinkSync: ''
|
|
5784
|
+
},
|
|
5785
|
+
os: { platform: 'string', homedir: 'string', tmpdir: 'string' },
|
|
5786
|
+
url: { fileURLToPath: 'string', pathToFileURL: 'string' },
|
|
5787
|
+
child_process: { execSync: 'string' },
|
|
5788
|
+
readline: { question: 'string' }
|
|
5789
|
+
};
|
|
5790
|
+
// Result types of imported Node.js functions by their local callee text
|
|
5791
|
+
// (`join`, `path.join`)
|
|
5792
|
+
const nodeCallResultTypes = new Map();
|
|
2994
5793
|
// Mapping from Node.js stdlib module names to Go setup functions.
|
|
2995
5794
|
// Each entry adds the required Go imports and registers call handlers for the local identifier.
|
|
2996
5795
|
function setupNodeModuleImport(node, nodeModule) {
|
|
@@ -3009,6 +5808,9 @@ function setupNodeModuleImport(node, nodeModule) {
|
|
|
3009
5808
|
continue;
|
|
3010
5809
|
const localName = el.name.text;
|
|
3011
5810
|
const importedName = el.propertyName?.text ?? localName;
|
|
5811
|
+
const resultType = NODE_FUNCTION_TYPES[nodeModule]?.[importedName];
|
|
5812
|
+
if (resultType !== undefined)
|
|
5813
|
+
nodeCallResultTypes.set(localName, resultType);
|
|
3012
5814
|
const fn = mapping.functions[importedName];
|
|
3013
5815
|
if (fn) {
|
|
3014
5816
|
// Register a call handler keyed on the local name
|
|
@@ -3020,6 +5822,9 @@ function setupNodeModuleImport(node, nodeModule) {
|
|
|
3020
5822
|
// Default import (`import path from 'node:path'`) or
|
|
3021
5823
|
// namespace import (`import * as path from 'node:path'`)
|
|
3022
5824
|
const localName = getImportLocalName(node) ?? nodeModule;
|
|
5825
|
+
for (const [funcName, resultType] of Object.entries(NODE_FUNCTION_TYPES[nodeModule] ?? {})) {
|
|
5826
|
+
nodeCallResultTypes.set(`${localName}.${funcName}`, resultType);
|
|
5827
|
+
}
|
|
3023
5828
|
for (const [funcName, fn] of Object.entries(mapping.functions)) {
|
|
3024
5829
|
callHandlers[`${localName}.${funcName}`] = (_caller, args) => fn(args);
|
|
3025
5830
|
}
|
|
@@ -3082,6 +5887,93 @@ function visitImportDeclaration(node) {
|
|
|
3082
5887
|
}
|
|
3083
5888
|
return '';
|
|
3084
5889
|
}
|
|
5890
|
+
// Records the type of a variable introduced by a loop or binding
|
|
5891
|
+
function registerLocalVariable(name, goType) {
|
|
5892
|
+
if (!name || name === '_')
|
|
5893
|
+
return;
|
|
5894
|
+
variableTypes.delete(name);
|
|
5895
|
+
variableClassNames.delete(name);
|
|
5896
|
+
variableTypeNodes.delete(name);
|
|
5897
|
+
narrowedVariables.delete(name);
|
|
5898
|
+
if (goType)
|
|
5899
|
+
variableGoTypes.set(name, goType);
|
|
5900
|
+
else
|
|
5901
|
+
variableGoTypes.delete(name);
|
|
5902
|
+
}
|
|
5903
|
+
// for...of over arrays and strings (a string yields one-character strings)
|
|
5904
|
+
function visitForOfSequence(node, iterExpr, iterType) {
|
|
5905
|
+
if (iterType === 'interface{}')
|
|
5906
|
+
iterType = '[]interface{}';
|
|
5907
|
+
const elementType = iterType === 'string' ? 'string' : iterType?.startsWith('[]') ? iterType.slice(2) : undefined;
|
|
5908
|
+
const declaration = isVariableDeclarationList(node.initializer)
|
|
5909
|
+
? node.initializer.declarations[0]
|
|
5910
|
+
: undefined;
|
|
5911
|
+
const binding = declaration?.name;
|
|
5912
|
+
const item = getTempName('item');
|
|
5913
|
+
let prefix = '';
|
|
5914
|
+
let loopVar = item;
|
|
5915
|
+
if (binding && isArrayBindingPattern(binding)) {
|
|
5916
|
+
// for (const [a, b] of pairs) → a := item[0]; b := item[1]
|
|
5917
|
+
const memberType = elementType?.startsWith('[]') ? elementType.slice(2) : undefined;
|
|
5918
|
+
(binding.elements ?? []).forEach((el, index) => {
|
|
5919
|
+
if (isOmittedExpression(el))
|
|
5920
|
+
return;
|
|
5921
|
+
const name = visit(el.name);
|
|
5922
|
+
registerLocalVariable(name, memberType);
|
|
5923
|
+
prefix += `${name} := ${item}[${index}]\n\t\t_ = ${name}\n\t\t`;
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
else if (binding && isIdentifier(binding)) {
|
|
5927
|
+
const name = visit(binding);
|
|
5928
|
+
registerLocalVariable(binding.text, elementType);
|
|
5929
|
+
if (iterType === 'string') {
|
|
5930
|
+
prefix = `${name} := string(${item})\n\t\t`;
|
|
5931
|
+
}
|
|
5932
|
+
else {
|
|
5933
|
+
loopVar = name;
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
else {
|
|
5937
|
+
loopVar = visit(node.initializer, { inline: true });
|
|
5938
|
+
}
|
|
5939
|
+
return `for _, ${loopVar} := range ${iterExpr}${visitLoopBody(node.statement, prefix)}`;
|
|
5940
|
+
}
|
|
5941
|
+
// try/catch with returns: the closure reports (value, done) and the caller
|
|
5942
|
+
// returns when done
|
|
5943
|
+
function visitReturningTryStatement(node, fn, options) {
|
|
5944
|
+
const returnGoType = inferFunctionBodyReturnType(fn);
|
|
5945
|
+
const hasValue = !!returnGoType;
|
|
5946
|
+
const saved = tryReturn;
|
|
5947
|
+
const retVar = getTempName('tryret');
|
|
5948
|
+
const doneVar = getTempName('trydone');
|
|
5949
|
+
const blockCode = (block, inCatch) => {
|
|
5950
|
+
tryReturn = { fn, hasValue, inCatch, retVar, doneVar };
|
|
5951
|
+
const code = (block.statements ?? []).map((s) => visit(s)).join('\t');
|
|
5952
|
+
tryReturn = saved;
|
|
5953
|
+
return code;
|
|
5954
|
+
};
|
|
5955
|
+
const deferreds = [];
|
|
5956
|
+
if (node.finallyBlock) {
|
|
5957
|
+
const finallyBody = (node.finallyBlock.statements ?? []).map((s) => visit(s)).join('\t');
|
|
5958
|
+
deferreds.push(`defer func() {\n\t\t\t${finallyBody}\t\t\t}()`);
|
|
5959
|
+
}
|
|
5960
|
+
if (node.catchClause) {
|
|
5961
|
+
const varDecl = node.catchClause.variableDeclaration;
|
|
5962
|
+
const catchVar = varDecl ? visit(varDecl.name) : '_r';
|
|
5963
|
+
const catchBody = blockCode(node.catchClause.block, true);
|
|
5964
|
+
deferreds.push(`defer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\t${catchVar} := r\n\t\t\t\t_ = ${catchVar}\n\t\t\t\t${catchBody}\t\t\t}\n\t\t\t}()`);
|
|
5965
|
+
}
|
|
5966
|
+
const tryBody = blockCode(node.tryBlock, false);
|
|
5967
|
+
const results = hasValue
|
|
5968
|
+
? `(${retVar} ${returnGoType}, ${doneVar} bool)`
|
|
5969
|
+
: `(${doneVar} bool)`;
|
|
5970
|
+
const body = [...deferreds, tryBody, 'return'].join('\n\t\t\t');
|
|
5971
|
+
const outer = hasValue ? `${retVar}, ${doneVar}` : doneVar;
|
|
5972
|
+
// Leaving through an enclosing try/catch closure of the same function passes the value on
|
|
5973
|
+
const exit = tryAwareReturn(hasValue ? retVar : '');
|
|
5974
|
+
return (`if ${outer} := func() ${results} {\n\t\t\t${body}\n\t\t\t}(); ${doneVar} { ${exit} }` +
|
|
5975
|
+
(options.inline ? '' : ';\n\t'));
|
|
5976
|
+
}
|
|
3085
5977
|
function getForOfVarNames(initializer) {
|
|
3086
5978
|
if (!isVariableDeclarationList(initializer) || initializer.declarations.length === 0) {
|
|
3087
5979
|
return ['_'];
|
|
@@ -3096,7 +5988,27 @@ function getForOfVarNames(initializer) {
|
|
|
3096
5988
|
}
|
|
3097
5989
|
return [visit(decl.name)];
|
|
3098
5990
|
}
|
|
5991
|
+
// Set while emitting a try/catch whose blocks return from the enclosing function
|
|
5992
|
+
let tryReturn;
|
|
5993
|
+
// `return value` from inside a try/catch closure: report (value, done)
|
|
5994
|
+
function tryAwareReturn(value) {
|
|
5995
|
+
if (!tryReturn)
|
|
5996
|
+
return `return ${value}`;
|
|
5997
|
+
const results = tryReturn.hasValue ? `${value}, true` : 'true';
|
|
5998
|
+
if (tryReturn.inCatch) {
|
|
5999
|
+
const targets = tryReturn.hasValue
|
|
6000
|
+
? `${tryReturn.retVar}, ${tryReturn.doneVar}`
|
|
6001
|
+
: tryReturn.doneVar;
|
|
6002
|
+
return `${targets} = ${results}; return`;
|
|
6003
|
+
}
|
|
6004
|
+
return `return ${results}`;
|
|
6005
|
+
}
|
|
3099
6006
|
function visitTryStatement(node, options) {
|
|
6007
|
+
const fn = getEnclosingFunction(node);
|
|
6008
|
+
const returnsFromFunction = (block) => !!block && containsNode(block, (n) => isReturnStatement(n) && getEnclosingFunction(n) === fn);
|
|
6009
|
+
if (fn && (returnsFromFunction(node.tryBlock) || returnsFromFunction(node.catchClause?.block))) {
|
|
6010
|
+
return visitReturningTryStatement(node, fn, options);
|
|
6011
|
+
}
|
|
3100
6012
|
const deferreds = [];
|
|
3101
6013
|
// Register finally first (LIFO: runs last after catch)
|
|
3102
6014
|
if (node.finallyBlock) {
|