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
132 changes: 99 additions & 33 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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[] = [];

Expand Down
9 changes: 9 additions & 0 deletions src/TSHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
123 changes: 123 additions & 0 deletions test/unit/taggedTemplateLiterals.spec.ts
Original file line number Diff line number Diff line change
@@ -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'}`",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it always calls functions with global context:

const obj = {
    func(strings: TemplateStringsArray, ...expressions: any[]) {
        console.log(this === obj); // `true` in JS, `false` in Lua
    },
};

obj.func`hello`;
local obj = {func = function(self, strings, ...)
    print(self == obj)
end}
obj.func(_G, { -- should be `obj`
    "hello",
    raw = {"hello"},
})

Also there is a case where obj isn't pure:

getObj()["func"]`hello`;

In call expressions it's handled like that:

(function()
    local ____TS_self = getObj(_G)
    return ____TS_self.func(____TS_self, {
        "hello",
        raw = {"hello"},
    })
end)()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find! Took a bit to understand what to do here, I've added transformContextualCallExpression in an attempt to preserve this left hand side behaviour to call-like expressions which includes these template literals.

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");
}
);