Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions src/baseTranspiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { IFileImport, IFileExport, TranspilationError, IMethodType, IParameterType } from './types.js';
import { unCamelCase } from "./utils.js";
import { Logger } from "./logger.js";
import { timingSafeEqual } from 'crypto';

Check warning on line 5 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'timingSafeEqual' is defined but never used
class BaseTranspiler {

NUM_LINES_BETWEEN_CLASS_MEMBERS = 1;
Expand Down Expand Up @@ -190,6 +190,7 @@

uncamelcaseIdentifiers;
asyncTranspiling;
implicitAsyncTranspiling;
requiresReturnType;
requiresParameterType;
supportsFalsyOrTruthyValues;
Expand Down Expand Up @@ -310,14 +311,28 @@
Logger.warning(`[${this.id}] Line: ${line} char: ${character}: ${target} : ${message}`);
}

isAsyncFunction(node) {
let modifiers = node.modifiers;
if (modifiers === undefined) {
hasAsyncModifier(node) {
return (node.modifiers ?? []).some(mod => mod.kind === ts.SyntaxKind.AsyncKeyword);
}

isPromiseType(type) {
return type !== undefined && this.getTypeFromRawType(type) === this.PROMISE_TYPE_KEYWORD;
}

isImplicitAsyncFunction(node) {
// a function declared WITHOUT the `async` keyword that returns a Promise
// directly, ie: `watchTicker (symbol: string): Promise<Ticker> { return this.watch (...); }`
// (a common fast-path in JS for pure delegator/pass-through methods);
// target languages without that distinction transpile it as if it were `async`
if (!this.implicitAsyncTranspiling || !ts.isFunctionLike(node) || this.hasAsyncModifier(node)) {
return false;
}
modifiers = modifiers.filter(mod => mod.kind === ts.SyntaxKind.AsyncKeyword);
const signature = global.checker.getSignatureFromDeclaration(node);
return signature !== undefined && this.isPromiseType(global.checker.getReturnTypeOfSignature(signature));
}

return modifiers.length > 0;
isAsyncFunction(node) {
return this.hasAsyncModifier(node) || this.isImplicitAsyncFunction(node);
}

getMethodOverride(node: ts.Node): ts.Node {
Expand All @@ -343,7 +358,7 @@

let method = undefined;

let parentClass = (ts as any).getAllSuperTypeNodes(node.parent)[0];

Check warning on line 361 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected any. Specify a different type

while (parentClass !== undefined) {
const parentClassType = global.checker.getTypeAtLocation(parentClass);
Expand All @@ -360,13 +375,13 @@
if (ts.isMethodDeclaration(elem)) {

const name = elem.name.getText().trim();
if ((node as any).name.escapedText === name) {

Check warning on line 378 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected any. Specify a different type
method = elem;
}
}
});

parentClass = (ts as any).getAllSuperTypeNodes(parentClassDecl)[0] ?? undefined;

Check warning on line 384 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected any. Specify a different type
}


Expand All @@ -379,7 +394,7 @@
return this.DEFAULT_IDENTATION.repeat(parseInt(num));
}

getBlockOpen(identation){

Check warning on line 397 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'identation' is defined but never used
return this.SPACE_BEFORE_BLOCK_OPENING + this.BLOCK_OPENING_TOKEN + "\n";
}

Expand Down Expand Up @@ -444,11 +459,11 @@
return this.getIden(identation) + `${left} instanceof ${right}`;
}

getCustomOperatorIfAny(left, right, operator) {

Check warning on line 462 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'operator' is defined but never used

Check warning on line 462 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'right' is defined but never used

Check warning on line 462 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'left' is defined but never used
return undefined;
}

printCustomBinaryExpressionIfAny(node, identation) {

Check warning on line 466 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'identation' is defined but never used

Check warning on line 466 in src/baseTranspiler.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'node' is defined but never used
return undefined; // stub to override
}

Expand Down Expand Up @@ -607,16 +622,19 @@
}

printModifiers(node) {
let modifiers = node.modifiers;
if (modifiers === undefined) {
return "";
}
let modifiers = node.modifiers ?? [];
modifiers = modifiers.filter(mod => this.FuncModifiers[mod.kind]);

if (!this.asyncTranspiling) {
modifiers = modifiers.filter(mod => mod.kind !== ts.SyntaxKind.AsyncKeyword);
}
const res = modifiers.map(modifier => this.FuncModifiers[modifier.kind]).join(" ");
let res = modifiers.map(modifier => this.FuncModifiers[modifier.kind]).join(" ");

if (this.asyncTranspiling && this.ASYNC_TOKEN && this.isImplicitAsyncFunction(node)) {
// non-async functions returning a Promise directly are transpiled
// as if they had been declared `async`
res = res ? res + " " + this.ASYNC_TOKEN : this.ASYNC_TOKEN;
}

return res;
}
Expand Down Expand Up @@ -1663,6 +1681,35 @@
return awaitToken + expression;
}

wrapSyntheticNode(synthetic, original) {
// give the synthetic wrapper the original node's source range and splice it
// into the parent chain, so position/type based helpers keep working
synthetic.pos = original.pos;
synthetic.end = original.end;
synthetic.parent = original.parent;
original.parent = synthetic;
return synthetic;
}

wrapImplicitReturnAwait(node) {
// inside a non-async function transpiled as async (isImplicitAsyncFunction),
// returned Promises must be awaited to keep the generated code equivalent to
// the classic `async`/`return await` form: wrap them in a synthetic await
// expression so every language prints them through its usual await path
let exp = node.expression;
if (!exp || ts.isAwaitExpression(exp)
|| !this.isImplicitAsyncFunction(ts.findAncestor(node, ts.isFunctionLike))
|| !this.isPromiseType(global.checker.getTypeAtLocation(exp))) {
return node;
}
if (ts.isConditionalExpression(exp) || ts.isBinaryExpression(exp)) {
// an await-token prefix would change precedence (e.g. `await a ? b : c`)
exp = this.wrapSyntheticNode(ts.factory.createParenthesizedExpression(exp), exp);
}
node.expression = this.wrapSyntheticNode(ts.factory.createAwaitExpression(exp), exp);
return node;
}

printConditionalExpression(node, identation) {
const condition = this.printCondition(node.condition, identation);
const whenTrue = this.printNode(node.whenTrue, 0);
Expand Down Expand Up @@ -1856,7 +1903,7 @@
} else if (ts.isAsExpression(node)) {
return this.printAsExpression(node, identation);
} else if (ts.isReturnStatement(node)) {
return this.printReturnStatement(node, identation);
return this.printReturnStatement(this.wrapImplicitReturnAwait(node), identation);
} else if (ts.isArrayBindingPattern(node)) {
return this.printArrayBindingPattern(node, identation);
} else if (ts.isParameter(node)) {
Expand Down
1 change: 1 addition & 0 deletions src/csharpTranspiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class CSharpTranspiler extends BaseTranspiler {
this.requiresParameterType = true;
this.requiresReturnType = true;
this.asyncTranspiling = true;
this.implicitAsyncTranspiling = true;
this.supportsFalsyOrTruthyValues = false;
this.requiresCallExpressionCast = true;
this.id = "C#";
Expand Down
3 changes: 2 additions & 1 deletion src/goTranspiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class GoTranspiler extends BaseTranspiler {
this.requiresParameterType = true;
this.requiresReturnType = true;
this.asyncTranspiling = false;
this.implicitAsyncTranspiling = true;
this.supportsFalsyOrTruthyValues = false;
this.requiresCallExpressionCast = true;
this.wrapThisCalls = false;
Expand Down Expand Up @@ -1148,7 +1149,7 @@ ${this.getIden(identation)}PanicOnError(${returnRandName})`;
ts.isFunctionExpression(currentNode) ||
ts.isArrowFunction(currentNode) ||
ts.isMethodDeclaration(currentNode)) {
return currentNode.modifiers && currentNode.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword);
return this.isAsyncFunction(currentNode);
}
// Move up the tree to the parent node
currentNode = currentNode.parent;
Expand Down
1 change: 1 addition & 0 deletions src/javaTranspiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class JavaTranspiler extends BaseTranspiler {
this.requiresParameterType = true;
this.requiresReturnType = true;
this.asyncTranspiling = true;
this.implicitAsyncTranspiling = true;
this.supportsFalsyOrTruthyValues = false;
this.requiresCallExpressionCast = true;
this.id = "Java";
Expand Down
31 changes: 31 additions & 0 deletions tests/csharpTranspiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -991,4 +991,35 @@ describe('csharp transpiling tests', () => {
const output = transpiler.transpileCSharp(ts).content;
expect(output).toBe(csharp);
});
test('non-async Promise-returning delegator transpiles like async return await', () => {
// a method without `async` that returns a Promise (e.g. WS delegators
// like `watchTicker(...) { return this.watchTickerInner(...); }`)
// must produce the exact same C# as its `async`/`return await` twin:
// `async ... Task<object>` signature + `return await ...`.
const input =
"class Exchange {\n" +
" async watchTickerInner(symbol: string): Promise<any> {\n" +
" return { 'symbol': symbol };\n" +
" }\n" +
" watchTicker(symbol: string): Promise<any> {\n" +
" return this.watchTickerInner(symbol);\n" +
" }\n" +
" async watchTickerClassic(symbol: string): Promise<any> {\n" +
" return await this.watchTickerInner(symbol);\n" +
" }\n" +
"}"
const output = transpiler.transpileCSharp(input).content;
expect(output).toContain("public async virtual Task<object> watchTicker(object symbol)");
expect(output).toContain("return await this.watchTickerInner(symbol);");
// must not return the bare Task without awaiting it
expect(output).not.toContain("return this.watchTickerInner(symbol);");
// delegator body must be identical to the classic async/return await version
const getBody = (name: string) => {
const start = output.indexOf(`Task<object> ${name}(`);
const open = output.indexOf('{', start);
const close = output.indexOf('}', open);
return output.slice(open, close + 1);
};
expect(getBody('watchTicker')).toBe(getBody('watchTickerClassic'));
});
});
37 changes: 37 additions & 0 deletions tests/goTranspiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,41 @@ describe('go transpiling tests', () => {
const output = transpiler.transpileGo(ts).content;
expect(output).toBe(go);
});
test('non-async Promise-returning delegator transpiles like async return await', () => {
// a method without `async` that returns a Promise (e.g. WS delegators
// like `watchTicker(...) { return this.watchTickerInner(...); }`)
// must produce the exact same Go as its `async`/`return await` twin:
// channel-wrapped body with `<-` receive + PanicOnError on the result.
const input =
"class Exchange {\n" +
" async watchTickerInner(symbol: string): Promise<any> {\n" +
" return { 'symbol': symbol };\n" +
" }\n" +
" watchTicker(symbol: string): Promise<any> {\n" +
" return this.watchTickerInner(symbol);\n" +
" }\n" +
" async watchTickerClassic(symbol: string): Promise<any> {\n" +
" return await this.watchTickerInner(symbol);\n" +
" }\n" +
"}"
const output = transpiler.transpileGo(input).content;
// extract each method body
const methods = output.split(/func\s+\(this \*Exchange\)/).slice(1);
expect(methods.length).toBe(3);
const [inner, delegator, classic] = methods;
// the delegator must be channel-wrapped and receive from the inner channel
expect(delegator).toContain("ch := make(chan any)");
expect(delegator).toContain("<-this.WatchTickerInner(symbol)");
expect(delegator).toContain("PanicOnError(retRes");
// must NOT return the raw channel of the inner call
expect(delegator).not.toContain("ch <- this.WatchTickerInner");
// normalized (method name + line-based retRes suffix stripped), the
// delegator must be identical to the classic async/return await version
const normalize = (s: string) => s
.replace(/retRes\d+/g, 'retRes')
.replace(/WatchTickerClassic|WatchTicker\b/g, 'METHOD')
.trim();
expect(normalize(delegator)).toBe(normalize(classic));
void inner;
});
});
32 changes: 32 additions & 0 deletions tests/javaTranspiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2196,4 +2196,36 @@ describe('java transpiling tests', () => {
expect((output.match(/final Object finalI\b/g) || []).length).toBe(2);
expect((output.match(/final Object finalType/g) || []).length).toBe(1);
});
test('non-async Promise-returning delegator transpiles like async return await', () => {
// a method without `async` that returns a Promise (e.g. WS delegators
// like `watchTicker(...) { return this.watchTickerInner(...); }`)
// must produce the exact same Java as its `async`/`return await` twin:
// CompletableFuture signature + supplyAsync wrapper + `.join()` on the
// inner future (instead of returning the raw CompletableFuture, which
// would make the outer future resolve to a future).
const input =
"class Exchange {\n" +
" async watchTickerInner(symbol: string): Promise<any> {\n" +
" return { 'symbol': symbol };\n" +
" }\n" +
" watchTicker(symbol: string): Promise<any> {\n" +
" return this.watchTickerInner(symbol);\n" +
" }\n" +
" async watchTickerClassic(symbol: string): Promise<any> {\n" +
" return await this.watchTickerInner(symbol);\n" +
" }\n" +
"}"
const output = transpiler.transpileJava(input).content;
expect(output).toContain("java.util.concurrent.CompletableFuture<Object> watchTicker(");
expect(output).toContain("return (this.watchTickerInner(symbol)).join();");
// the delegator must be wrapped in supplyAsync like any async method
expect((output.match(/supplyAsync/g) || []).length).toBe(3);
// delegator body must be identical to the classic async/return await version
const getBody = (name: string) => {
const start = output.indexOf(`CompletableFuture<Object> ${name}(`);
const end = output.indexOf('});', start);
return output.slice(output.indexOf('{', start), end);
};
expect(getBody('watchTicker')).toBe(getBody('watchTickerClassic'));
});
});
Loading