Skip to content

Commit eb7ae08

Browse files
hazzard993Perryvw
authored andcommitted
Tagged template literals support (#638)
* Implement Tagged Template Literals * Add unicode escape test * Keep track of raw strings * Improve raw string preservation and use type narrowing * Simplfy method using TypeScript's example * Fix ContextType reference * Remove unnecessary check * Reduce checks and use filterUndefinedAndCast * Add transformContextualCallExpression to transform call expression left hand side
1 parent 95da8fe commit eb7ae08

File tree

3 files changed

+231
-33
lines changed

3 files changed

+231
-33
lines changed

src/LuaTransformer.ts

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2683,6 +2683,8 @@ export class LuaTransformer {
26832683
case ts.SyntaxKind.StringLiteral:
26842684
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
26852685
return this.transformStringLiteral(expression as ts.StringLiteral);
2686+
case ts.SyntaxKind.TaggedTemplateExpression:
2687+
return this.transformTaggedTemplateExpression(expression as ts.TaggedTemplateExpression);
26862688
case ts.SyntaxKind.TemplateExpression:
26872689
return this.transformTemplateExpression(expression as ts.TemplateExpression);
26882690
case ts.SyntaxKind.NumericLiteral:
@@ -3835,20 +3837,8 @@ export class LuaTransformer {
38353837
!signatureDeclaration ||
38363838
tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== tsHelper.ContextType.Void
38373839
) {
3838-
if (
3839-
luaKeywords.has(node.expression.name.text) ||
3840-
!tsHelper.isValidLuaIdentifier(node.expression.name.text)
3841-
) {
3842-
return this.transformElementCall(node);
3843-
} else {
3844-
// table:name()
3845-
return tstl.createMethodCallExpression(
3846-
table,
3847-
this.transformIdentifier(node.expression.name),
3848-
parameters,
3849-
node
3850-
);
3851-
}
3840+
// table:name()
3841+
return this.transformContextualCallExpression(node, parameters);
38523842
} else {
38533843
// table.name()
38543844
const callPath = tstl.createTableIndexExpression(
@@ -3868,43 +3858,67 @@ export class LuaTransformer {
38683858
}
38693859

38703860
const signature = this.checker.getResolvedSignature(node);
3871-
let parameters = this.transformArguments(node.arguments, signature);
3872-
38733861
const signatureDeclaration = signature && signature.getDeclaration();
3862+
const parameters = this.transformArguments(node.arguments, signature);
38743863
if (
38753864
!signatureDeclaration ||
38763865
tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== tsHelper.ContextType.Void
38773866
) {
3878-
// Pass left-side as context
3867+
// A contextual parameter must be given to this call expression
3868+
return this.transformContextualCallExpression(node, parameters);
3869+
} else {
3870+
// No context
3871+
const expression = this.transformExpression(node.expression);
3872+
return tstl.createCallExpression(expression, parameters);
3873+
}
3874+
}
38793875

3880-
const context = this.transformExpression(node.expression.expression);
3881-
if (tsHelper.isExpressionWithEvaluationEffect(node.expression.expression)) {
3876+
public transformContextualCallExpression(
3877+
node: ts.CallExpression | ts.TaggedTemplateExpression,
3878+
transformedArguments: tstl.Expression[]
3879+
): ExpressionVisitResult {
3880+
const left = ts.isCallExpression(node) ? node.expression : node.tag;
3881+
const leftHandSideExpression = this.transformExpression(left);
3882+
if (
3883+
ts.isPropertyAccessExpression(left) &&
3884+
!luaKeywords.has(left.name.text) &&
3885+
tsHelper.isValidLuaIdentifier(left.name.text)
3886+
) {
3887+
// table:name()
3888+
const table = this.transformExpression(left.expression);
3889+
return tstl.createMethodCallExpression(
3890+
table,
3891+
this.transformIdentifier(left.name),
3892+
transformedArguments,
3893+
node
3894+
);
3895+
} else if (ts.isElementAccessExpression(left) || ts.isPropertyAccessExpression(left)) {
3896+
const context = this.transformExpression(left.expression);
3897+
if (tsHelper.isExpressionWithEvaluationEffect(left.expression)) {
38823898
// Inject context parameter
3883-
if (node.arguments.length > 0) {
3884-
parameters.unshift(tstl.createIdentifier("____TS_self"));
3885-
} else {
3886-
parameters = [tstl.createIdentifier("____TS_self")];
3887-
}
3899+
transformedArguments.unshift(tstl.createIdentifier("____TS_self"));
38883900

38893901
// Cache left-side if it has effects
38903902
//(function() local ____TS_self = context; return ____TS_self[argument](parameters); end)()
3891-
const argumentExpression = ts.isElementAccessExpression(node.expression)
3892-
? node.expression.argumentExpression
3893-
: ts.createStringLiteral(node.expression.name.text);
3903+
const argumentExpression = ts.isElementAccessExpression(left)
3904+
? left.argumentExpression
3905+
: ts.createStringLiteral(left.name.text);
38943906
const argument = this.transformExpression(argumentExpression);
38953907
const selfIdentifier = tstl.createIdentifier("____TS_self");
38963908
const selfAssignment = tstl.createVariableDeclarationStatement(selfIdentifier, context);
38973909
const index = tstl.createTableIndexExpression(selfIdentifier, argument);
3898-
const callExpression = tstl.createCallExpression(index, parameters);
3910+
const callExpression = tstl.createCallExpression(index, transformedArguments);
38993911
return this.createImmediatelyInvokedFunctionExpression([selfAssignment], callExpression, node);
39003912
} else {
3901-
const expression = this.transformExpression(node.expression);
3902-
return tstl.createCallExpression(expression, [context, ...parameters]);
3913+
const expression = this.transformExpression(left);
3914+
return tstl.createCallExpression(expression, [context, ...transformedArguments]);
39033915
}
3916+
} else if (ts.isIdentifier(left)) {
3917+
const context = this.isStrict ? tstl.createNilLiteral() : tstl.createIdentifier("_G");
3918+
transformedArguments.unshift(context);
3919+
return tstl.createCallExpression(leftHandSideExpression, transformedArguments, node);
39043920
} else {
3905-
// No context
3906-
const expression = this.transformExpression(node.expression);
3907-
return tstl.createCallExpression(expression, parameters);
3921+
throw TSTLErrors.UnsupportedKind("Left Hand Side Call Expression", left.kind, left);
39083922
}
39093923
}
39103924

@@ -4678,6 +4692,58 @@ export class LuaTransformer {
46784692
return this.createSelfIdentifier(thisKeyword);
46794693
}
46804694

4695+
public transformTaggedTemplateExpression(expression: ts.TaggedTemplateExpression): ExpressionVisitResult {
4696+
const strings: string[] = [];
4697+
const rawStrings: string[] = [];
4698+
const expressions: ts.Expression[] = [];
4699+
4700+
if (ts.isTemplateExpression(expression.template)) {
4701+
// Expressions are in the string.
4702+
strings.push(expression.template.head.text);
4703+
rawStrings.push(tsHelper.getRawLiteral(expression.template.head));
4704+
strings.push(...expression.template.templateSpans.map(span => span.literal.text));
4705+
rawStrings.push(...expression.template.templateSpans.map(span => tsHelper.getRawLiteral(span.literal)));
4706+
expressions.push(...expression.template.templateSpans.map(span => span.expression));
4707+
} else {
4708+
// No expressions are in the string.
4709+
strings.push(expression.template.text);
4710+
rawStrings.push(tsHelper.getRawLiteral(expression.template));
4711+
}
4712+
4713+
// Construct table with strings and literal strings
4714+
const stringTableLiteral = tstl.createTableExpression(
4715+
strings.map(partialString => tstl.createTableFieldExpression(tstl.createStringLiteral(partialString)))
4716+
);
4717+
if (stringTableLiteral.fields) {
4718+
const rawStringArray = tstl.createTableExpression(
4719+
rawStrings.map(stringLiteral =>
4720+
tstl.createTableFieldExpression(tstl.createStringLiteral(stringLiteral))
4721+
)
4722+
);
4723+
stringTableLiteral.fields.push(
4724+
tstl.createTableFieldExpression(rawStringArray, tstl.createStringLiteral("raw"))
4725+
);
4726+
}
4727+
4728+
// Evaluate if there is a self parameter to be used.
4729+
const signature = this.checker.getResolvedSignature(expression);
4730+
const signatureDeclaration = signature && signature.getDeclaration();
4731+
const useSelfParameter =
4732+
signatureDeclaration &&
4733+
tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== tsHelper.ContextType.Void;
4734+
4735+
// Argument evaluation.
4736+
const callArguments = this.transformArguments(expressions, signature);
4737+
callArguments.unshift(stringTableLiteral);
4738+
4739+
if (useSelfParameter) {
4740+
return this.transformContextualCallExpression(expression, callArguments);
4741+
}
4742+
4743+
const leftHandSideExpression = this.transformExpression(expression.tag);
4744+
return tstl.createCallExpression(leftHandSideExpression, callArguments);
4745+
}
4746+
46814747
public transformTemplateExpression(expression: ts.TemplateExpression): ExpressionVisitResult {
46824748
const parts: tstl.Expression[] = [];
46834749

src/TSHelper.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,15 @@ export function getFirstDeclaration(symbol: ts.Symbol, sourceFile?: ts.SourceFil
742742
return declarations.length > 0 ? declarations.reduce((p, c) => (p.pos < c.pos ? p : c)) : undefined;
743743
}
744744

745+
export function getRawLiteral(node: ts.LiteralLikeNode): string {
746+
let text = node.getText();
747+
const isLast =
748+
node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === ts.SyntaxKind.TemplateTail;
749+
text = text.substring(1, text.length - (isLast ? 1 : 2));
750+
text = text.replace(/\r\n?/g, "\n").replace(/\\/g, "\\\\");
751+
return text;
752+
}
753+
745754
export function isFirstDeclaration(node: ts.VariableDeclaration, checker: ts.TypeChecker): boolean {
746755
const symbol = checker.getSymbolAtLocation(node.name);
747756
if (!symbol) {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import * as util from "../util";
2+
3+
const testCases = [
4+
{
5+
callExpression: "func``",
6+
joinAllResult: "",
7+
joinRawResult: "",
8+
},
9+
{
10+
callExpression: "func`hello`",
11+
joinAllResult: "hello",
12+
joinRawResult: "hello",
13+
},
14+
{
15+
callExpression: "func`hello ${1} ${2} ${3}`",
16+
joinAllResult: "hello 1 2 3",
17+
joinRawResult: "hello ",
18+
},
19+
{
20+
callExpression: "func`hello ${(() => 'iife')()}`",
21+
joinAllResult: "hello iife",
22+
joinRawResult: "hello ",
23+
},
24+
{
25+
callExpression: "func`hello ${1 + 2 + 3} arithmetic`",
26+
joinAllResult: "hello 6 arithmetic",
27+
joinRawResult: "hello arithmetic",
28+
},
29+
{
30+
callExpression: "func`begin ${'middle'} end`",
31+
joinAllResult: "begin middle end",
32+
joinRawResult: "begin end",
33+
},
34+
{
35+
callExpression: "func`hello ${func`hello`}`",
36+
joinAllResult: "hello hello",
37+
joinRawResult: "hello ",
38+
},
39+
{
40+
callExpression: "func`hello \\u00A9`",
41+
joinAllResult: "hello ©",
42+
joinRawResult: "hello \\u00A9",
43+
},
44+
{
45+
callExpression: "func`hello $ { }`",
46+
joinAllResult: "hello $ { }",
47+
joinRawResult: "hello $ { }",
48+
},
49+
{
50+
callExpression: "func`hello { ${'brackets'} }`",
51+
joinAllResult: "hello { brackets }",
52+
joinRawResult: "hello { }",
53+
},
54+
{
55+
callExpression: "func`hello \\``",
56+
joinAllResult: "hello `",
57+
joinRawResult: "hello \\`",
58+
},
59+
{
60+
callExpression: "obj.func`hello ${'propertyAccessExpression'}`",
61+
joinAllResult: "hello propertyAccessExpression",
62+
joinRawResult: "hello ",
63+
},
64+
{
65+
callExpression: "obj['func']`hello ${'elementAccessExpression'}`",
66+
joinAllResult: "hello elementAccessExpression",
67+
joinRawResult: "hello ",
68+
},
69+
];
70+
71+
test.each(testCases)("TaggedTemplateLiteral call (%p)", ({ callExpression, joinAllResult }) => {
72+
const result = util.transpileAndExecute(`
73+
function func(strings: TemplateStringsArray, ...expressions: any[]) {
74+
const toJoin = [];
75+
for (let i = 0; i < strings.length; ++i) {
76+
if (strings[i]) {
77+
toJoin.push(strings[i]);
78+
}
79+
if (expressions[i]) {
80+
toJoin.push(expressions[i]);
81+
}
82+
}
83+
return toJoin.join("");
84+
}
85+
const obj = {
86+
func
87+
};
88+
return ${callExpression};
89+
`);
90+
91+
expect(result).toBe(joinAllResult);
92+
});
93+
94+
test.each(testCases)("TaggedTemplateLiteral raw preservation (%p)", ({ callExpression, joinRawResult }) => {
95+
const result = util.transpileAndExecute(`
96+
function func(strings: TemplateStringsArray, ...expressions: any[]) {
97+
return strings.raw.join("");
98+
}
99+
const obj = {
100+
func
101+
};
102+
return ${callExpression};
103+
`);
104+
105+
expect(result).toBe(joinRawResult);
106+
});
107+
108+
test.each(["func`noSelfParameter`", "obj.func`noSelfParameter`", "obj[`func`]`noSelfParameter`"])(
109+
"TaggedTemplateLiteral no self parameter",
110+
callExpression => {
111+
const result = util.transpileAndExecute(`
112+
function func(this: void, strings: TemplateStringsArray, ...expressions: any[]) {
113+
return strings.join("");
114+
}
115+
const obj = {
116+
func
117+
};
118+
return ${callExpression};
119+
`);
120+
121+
expect(result).toBe("noSelfParameter");
122+
}
123+
);

0 commit comments

Comments
 (0)