diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index e13a9a831..5bb13c9c1 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -2662,6 +2662,8 @@ export class LuaTransformer { case ts.SyntaxKind.StringLiteral: case ts.SyntaxKind.NoSubstitutionTemplateLiteral: return this.transformStringLiteral(expression as ts.StringLiteral); + case ts.SyntaxKind.TaggedTemplateExpression: + return this.transformTaggedTemplateExpression(expression as ts.TaggedTemplateExpression); case ts.SyntaxKind.TemplateExpression: return this.transformTemplateExpression(expression as ts.TemplateExpression); case ts.SyntaxKind.NumericLiteral: @@ -3814,20 +3816,8 @@ export class LuaTransformer { !signatureDeclaration || tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== tsHelper.ContextType.Void ) { - if ( - luaKeywords.has(node.expression.name.text) || - !tsHelper.isValidLuaIdentifier(node.expression.name.text) - ) { - return this.transformElementCall(node); - } else { - // table:name() - return tstl.createMethodCallExpression( - table, - this.transformIdentifier(node.expression.name), - parameters, - node - ); - } + // table:name() + return this.transformContextualCallExpression(node, parameters); } else { // table.name() const callPath = tstl.createTableIndexExpression( @@ -3847,43 +3837,67 @@ export class LuaTransformer { } const signature = this.checker.getResolvedSignature(node); - let parameters = this.transformArguments(node.arguments, signature); - const signatureDeclaration = signature && signature.getDeclaration(); + const parameters = this.transformArguments(node.arguments, signature); if ( !signatureDeclaration || tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== tsHelper.ContextType.Void ) { - // Pass left-side as context + // A contextual parameter must be given to this call expression + return this.transformContextualCallExpression(node, parameters); + } else { + // No context + const expression = this.transformExpression(node.expression); + return tstl.createCallExpression(expression, parameters); + } + } - const context = this.transformExpression(node.expression.expression); - if (tsHelper.isExpressionWithEvaluationEffect(node.expression.expression)) { + public transformContextualCallExpression( + node: ts.CallExpression | ts.TaggedTemplateExpression, + transformedArguments: tstl.Expression[] + ): ExpressionVisitResult { + const left = ts.isCallExpression(node) ? node.expression : node.tag; + const leftHandSideExpression = this.transformExpression(left); + if ( + ts.isPropertyAccessExpression(left) && + !luaKeywords.has(left.name.text) && + tsHelper.isValidLuaIdentifier(left.name.text) + ) { + // table:name() + const table = this.transformExpression(left.expression); + return tstl.createMethodCallExpression( + table, + this.transformIdentifier(left.name), + transformedArguments, + node + ); + } else if (ts.isElementAccessExpression(left) || ts.isPropertyAccessExpression(left)) { + const context = this.transformExpression(left.expression); + if (tsHelper.isExpressionWithEvaluationEffect(left.expression)) { // Inject context parameter - if (node.arguments.length > 0) { - parameters.unshift(tstl.createIdentifier("____TS_self")); - } else { - parameters = [tstl.createIdentifier("____TS_self")]; - } + transformedArguments.unshift(tstl.createIdentifier("____TS_self")); // Cache left-side if it has effects //(function() local ____TS_self = context; return ____TS_self[argument](parameters); end)() - const argumentExpression = ts.isElementAccessExpression(node.expression) - ? node.expression.argumentExpression - : ts.createStringLiteral(node.expression.name.text); + const argumentExpression = ts.isElementAccessExpression(left) + ? left.argumentExpression + : ts.createStringLiteral(left.name.text); const argument = this.transformExpression(argumentExpression); const selfIdentifier = tstl.createIdentifier("____TS_self"); const selfAssignment = tstl.createVariableDeclarationStatement(selfIdentifier, context); const index = tstl.createTableIndexExpression(selfIdentifier, argument); - const callExpression = tstl.createCallExpression(index, parameters); + const callExpression = tstl.createCallExpression(index, transformedArguments); return this.createImmediatelyInvokedFunctionExpression([selfAssignment], callExpression, node); } else { - const expression = this.transformExpression(node.expression); - return tstl.createCallExpression(expression, [context, ...parameters]); + const expression = this.transformExpression(left); + return tstl.createCallExpression(expression, [context, ...transformedArguments]); } + } else if (ts.isIdentifier(left)) { + const context = this.isStrict ? tstl.createNilLiteral() : tstl.createIdentifier("_G"); + transformedArguments.unshift(context); + return tstl.createCallExpression(leftHandSideExpression, transformedArguments, node); } else { - // No context - const expression = this.transformExpression(node.expression); - return tstl.createCallExpression(expression, parameters); + throw TSTLErrors.UnsupportedKind("Left Hand Side Call Expression", left.kind, left); } } @@ -4657,6 +4671,58 @@ export class LuaTransformer { return this.createSelfIdentifier(thisKeyword); } + public transformTaggedTemplateExpression(expression: ts.TaggedTemplateExpression): ExpressionVisitResult { + const strings: string[] = []; + const rawStrings: string[] = []; + const expressions: ts.Expression[] = []; + + if (ts.isTemplateExpression(expression.template)) { + // Expressions are in the string. + strings.push(expression.template.head.text); + rawStrings.push(tsHelper.getRawLiteral(expression.template.head)); + strings.push(...expression.template.templateSpans.map(span => span.literal.text)); + rawStrings.push(...expression.template.templateSpans.map(span => tsHelper.getRawLiteral(span.literal))); + expressions.push(...expression.template.templateSpans.map(span => span.expression)); + } else { + // No expressions are in the string. + strings.push(expression.template.text); + rawStrings.push(tsHelper.getRawLiteral(expression.template)); + } + + // Construct table with strings and literal strings + const stringTableLiteral = tstl.createTableExpression( + strings.map(partialString => tstl.createTableFieldExpression(tstl.createStringLiteral(partialString))) + ); + if (stringTableLiteral.fields) { + const rawStringArray = tstl.createTableExpression( + rawStrings.map(stringLiteral => + tstl.createTableFieldExpression(tstl.createStringLiteral(stringLiteral)) + ) + ); + stringTableLiteral.fields.push( + tstl.createTableFieldExpression(rawStringArray, tstl.createStringLiteral("raw")) + ); + } + + // Evaluate if there is a self parameter to be used. + const signature = this.checker.getResolvedSignature(expression); + const signatureDeclaration = signature && signature.getDeclaration(); + const useSelfParameter = + signatureDeclaration && + tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== tsHelper.ContextType.Void; + + // Argument evaluation. + const callArguments = this.transformArguments(expressions, signature); + callArguments.unshift(stringTableLiteral); + + if (useSelfParameter) { + return this.transformContextualCallExpression(expression, callArguments); + } + + const leftHandSideExpression = this.transformExpression(expression.tag); + return tstl.createCallExpression(leftHandSideExpression, callArguments); + } + public transformTemplateExpression(expression: ts.TemplateExpression): ExpressionVisitResult { const parts: tstl.Expression[] = []; diff --git a/src/TSHelper.ts b/src/TSHelper.ts index c8216b567..b587f99e9 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -742,6 +742,15 @@ export function getFirstDeclaration(symbol: ts.Symbol, sourceFile?: ts.SourceFil return declarations.length > 0 ? declarations.reduce((p, c) => (p.pos < c.pos ? p : c)) : undefined; } +export function getRawLiteral(node: ts.LiteralLikeNode): string { + let text = node.getText(); + const isLast = + node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === ts.SyntaxKind.TemplateTail; + text = text.substring(1, text.length - (isLast ? 1 : 2)); + text = text.replace(/\r\n?/g, "\n").replace(/\\/g, "\\\\"); + return text; +} + export function isFirstDeclaration(node: ts.VariableDeclaration, checker: ts.TypeChecker): boolean { const symbol = checker.getSymbolAtLocation(node.name); if (!symbol) { diff --git a/test/unit/taggedTemplateLiterals.spec.ts b/test/unit/taggedTemplateLiterals.spec.ts new file mode 100644 index 000000000..6ab4e68cd --- /dev/null +++ b/test/unit/taggedTemplateLiterals.spec.ts @@ -0,0 +1,123 @@ +import * as util from "../util"; + +const testCases = [ + { + callExpression: "func``", + joinAllResult: "", + joinRawResult: "", + }, + { + callExpression: "func`hello`", + joinAllResult: "hello", + joinRawResult: "hello", + }, + { + callExpression: "func`hello ${1} ${2} ${3}`", + joinAllResult: "hello 1 2 3", + joinRawResult: "hello ", + }, + { + callExpression: "func`hello ${(() => 'iife')()}`", + joinAllResult: "hello iife", + joinRawResult: "hello ", + }, + { + callExpression: "func`hello ${1 + 2 + 3} arithmetic`", + joinAllResult: "hello 6 arithmetic", + joinRawResult: "hello arithmetic", + }, + { + callExpression: "func`begin ${'middle'} end`", + joinAllResult: "begin middle end", + joinRawResult: "begin end", + }, + { + callExpression: "func`hello ${func`hello`}`", + joinAllResult: "hello hello", + joinRawResult: "hello ", + }, + { + callExpression: "func`hello \\u00A9`", + joinAllResult: "hello ©", + joinRawResult: "hello \\u00A9", + }, + { + callExpression: "func`hello $ { }`", + joinAllResult: "hello $ { }", + joinRawResult: "hello $ { }", + }, + { + callExpression: "func`hello { ${'brackets'} }`", + joinAllResult: "hello { brackets }", + joinRawResult: "hello { }", + }, + { + callExpression: "func`hello \\``", + joinAllResult: "hello `", + joinRawResult: "hello \\`", + }, + { + callExpression: "obj.func`hello ${'propertyAccessExpression'}`", + joinAllResult: "hello propertyAccessExpression", + joinRawResult: "hello ", + }, + { + callExpression: "obj['func']`hello ${'elementAccessExpression'}`", + joinAllResult: "hello elementAccessExpression", + joinRawResult: "hello ", + }, +]; + +test.each(testCases)("TaggedTemplateLiteral call (%p)", ({ callExpression, joinAllResult }) => { + const result = util.transpileAndExecute(` + function func(strings: TemplateStringsArray, ...expressions: any[]) { + const toJoin = []; + for (let i = 0; i < strings.length; ++i) { + if (strings[i]) { + toJoin.push(strings[i]); + } + if (expressions[i]) { + toJoin.push(expressions[i]); + } + } + return toJoin.join(""); + } + const obj = { + func + }; + return ${callExpression}; + `); + + expect(result).toBe(joinAllResult); +}); + +test.each(testCases)("TaggedTemplateLiteral raw preservation (%p)", ({ callExpression, joinRawResult }) => { + const result = util.transpileAndExecute(` + function func(strings: TemplateStringsArray, ...expressions: any[]) { + return strings.raw.join(""); + } + const obj = { + func + }; + return ${callExpression}; + `); + + expect(result).toBe(joinRawResult); +}); + +test.each(["func`noSelfParameter`", "obj.func`noSelfParameter`", "obj[`func`]`noSelfParameter`"])( + "TaggedTemplateLiteral no self parameter", + callExpression => { + const result = util.transpileAndExecute(` + function func(this: void, strings: TemplateStringsArray, ...expressions: any[]) { + return strings.join(""); + } + const obj = { + func + }; + return ${callExpression}; + `); + + expect(result).toBe("noSelfParameter"); + } +);