Skip to content

Commit cd0434a

Browse files
authored
fix(39744): make template literals more spec compliant (#45304)
* fix(39744): make template literals more spec compliant * Add evaluation test for template literals * Add test for template literals with source map
1 parent 0d2aeb7 commit cd0434a

File tree

102 files changed

+575
-522
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+575
-522
lines changed

src/compiler/transformers/es2015.ts

Lines changed: 12 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4102,87 +4102,22 @@ namespace ts {
41024102
* @param node A TemplateExpression node.
41034103
*/
41044104
function visitTemplateExpression(node: TemplateExpression): Expression {
4105-
const expressions: Expression[] = [];
4106-
addTemplateHead(expressions, node);
4107-
addTemplateSpans(expressions, node);
4108-
4109-
// createAdd will check if each expression binds less closely than binary '+'.
4110-
// If it does, it wraps the expression in parentheses. Otherwise, something like
4111-
// `abc${ 1 << 2 }`
4112-
// becomes
4113-
// "abc" + 1 << 2 + ""
4114-
// which is really
4115-
// ("abc" + 1) << (2 + "")
4116-
// rather than
4117-
// "abc" + (1 << 2) + ""
4118-
const expression = reduceLeft(expressions, factory.createAdd)!;
4119-
if (nodeIsSynthesized(expression)) {
4120-
setTextRange(expression, node);
4121-
}
4122-
4123-
return expression;
4124-
}
4125-
4126-
/**
4127-
* Gets a value indicating whether we need to include the head of a TemplateExpression.
4128-
*
4129-
* @param node A TemplateExpression node.
4130-
*/
4131-
function shouldAddTemplateHead(node: TemplateExpression) {
4132-
// If this expression has an empty head literal and the first template span has a non-empty
4133-
// literal, then emitting the empty head literal is not necessary.
4134-
// `${ foo } and ${ bar }`
4135-
// can be emitted as
4136-
// foo + " and " + bar
4137-
// This is because it is only required that one of the first two operands in the emit
4138-
// output must be a string literal, so that the other operand and all following operands
4139-
// are forced into strings.
4140-
//
4141-
// If the first template span has an empty literal, then the head must still be emitted.
4142-
// `${ foo }${ bar }`
4143-
// must still be emitted as
4144-
// "" + foo + bar
4145-
4146-
// There is always atleast one templateSpan in this code path, since
4147-
// NoSubstitutionTemplateLiterals are directly emitted via emitLiteral()
4148-
Debug.assert(node.templateSpans.length !== 0);
4105+
let expression: Expression = factory.createStringLiteral(node.head.text);
4106+
for (const span of node.templateSpans) {
4107+
const args = [visitNode(span.expression, visitor, isExpression)];
41494108

4150-
const span = node.templateSpans[0];
4151-
return node.head.text.length !== 0 || span.literal.text.length === 0 || !!length(getLeadingCommentRangesOfNode(span.expression, currentSourceFile));
4152-
}
4109+
if (span.literal.text.length > 0) {
4110+
args.push(factory.createStringLiteral(span.literal.text));
4111+
}
41534112

4154-
/**
4155-
* Adds the head of a TemplateExpression to an array of expressions.
4156-
*
4157-
* @param expressions An array of expressions.
4158-
* @param node A TemplateExpression node.
4159-
*/
4160-
function addTemplateHead(expressions: Expression[], node: TemplateExpression): void {
4161-
if (!shouldAddTemplateHead(node)) {
4162-
return;
4113+
expression = factory.createCallExpression(
4114+
factory.createPropertyAccessExpression(expression, "concat"),
4115+
/*typeArguments*/ undefined,
4116+
args,
4117+
);
41634118
}
41644119

4165-
expressions.push(factory.createStringLiteral(node.head.text));
4166-
}
4167-
4168-
/**
4169-
* Visits and adds the template spans of a TemplateExpression to an array of expressions.
4170-
*
4171-
* @param expressions An array of expressions.
4172-
* @param node A TemplateExpression node.
4173-
*/
4174-
function addTemplateSpans(expressions: Expression[], node: TemplateExpression): void {
4175-
for (const span of node.templateSpans) {
4176-
expressions.push(visitNode(span.expression, visitor, isExpression));
4177-
4178-
// Only emit if the literal is non-empty.
4179-
// The binary '+' operator is left-associative, so the first string concatenation
4180-
// with the head will force the result up to this point to be a string.
4181-
// Emitting a '+ ""' has no semantic effect for middles and tails.
4182-
if (span.literal.text.length !== 0) {
4183-
expressions.push(factory.createStringLiteral(span.literal.text));
4184-
}
4185-
}
4120+
return setTextRange(expression, node);
41864121
}
41874122

41884123
/**

src/testRunner/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"unittests/evaluation/optionalCall.ts",
101101
"unittests/evaluation/objectRest.ts",
102102
"unittests/evaluation/superInStaticInitializer.ts",
103+
"unittests/evaluation/templateLiteral.ts",
103104
"unittests/evaluation/updateExpressionInModule.ts",
104105
"unittests/services/cancellableLanguageServiceOperations.ts",
105106
"unittests/services/colorization.ts",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
describe("unittests:: evaluation:: templateLiteral", () => {
2+
it("toString() over valueOf()", () => {
3+
const result = evaluator.evaluateTypeScript(`
4+
class C {
5+
toString() {
6+
return "toString";
7+
}
8+
valueOf() {
9+
return "valueOf";
10+
}
11+
}
12+
13+
export const output = \`\${new C}\`;
14+
`);
15+
assert.strictEqual(result.output, "toString");
16+
});
17+
18+
it("correct evaluation order", () => {
19+
const result = evaluator.evaluateTypeScript(`
20+
class C {
21+
counter: number;
22+
23+
constructor() {
24+
this.counter = 0;
25+
}
26+
27+
get foo() {
28+
this.counter++;
29+
return {
30+
toString: () => this.counter++,
31+
};
32+
}
33+
}
34+
35+
const c = new C;
36+
export const output = \`\${c.foo} \${c.foo}\`;
37+
`);
38+
assert.strictEqual(result.output, "1 3");
39+
});
40+
});

tests/baselines/reference/APISample_compile.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ function compile(fileNames, options) {
6666
return;
6767
}
6868
var _a = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), line = _a.line, character = _a.character;
69-
console.log(diagnostic.file.fileName + " (" + (line + 1) + "," + (character + 1) + "): " + message);
69+
console.log("".concat(diagnostic.file.fileName, " (").concat(line + 1, ",").concat(character + 1, "): ").concat(message));
7070
});
7171
var exitCode = emitResult.emitSkipped ? 1 : 0;
72-
console.log("Process exiting with code '" + exitCode + "'.");
72+
console.log("Process exiting with code '".concat(exitCode, "'."));
7373
process.exit(exitCode);
7474
}
7575
exports.compile = compile;

tests/baselines/reference/APISample_linter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ function delint(sourceFile) {
114114
}
115115
function report(node, message) {
116116
var _a = sourceFile.getLineAndCharacterOfPosition(node.getStart()), line = _a.line, character = _a.character;
117-
console.log(sourceFile.fileName + " (" + (line + 1) + "," + (character + 1) + "): " + message);
117+
console.log("".concat(sourceFile.fileName, " (").concat(line + 1, ",").concat(character + 1, "): ").concat(message));
118118
}
119119
}
120120
exports.delint = delint;

tests/baselines/reference/APISample_parseConfig.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function printError(error) {
5656
if (!error) {
5757
return;
5858
}
59-
console.log((error.file && error.file.fileName) + ": " + error.messageText);
59+
console.log("".concat(error.file && error.file.fileName, ": ").concat(error.messageText));
6060
}
6161
function createProgram(rootFiles, compilerOptionsJson) {
6262
var _a = ts.parseConfigFileTextToJson("tsconfig.json", compilerOptionsJson), config = _a.config, error = _a.error;

tests/baselines/reference/APISample_watcher.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,10 @@ function watch(rootFileNames, options) {
166166
function emitFile(fileName) {
167167
var output = services.getEmitOutput(fileName);
168168
if (!output.emitSkipped) {
169-
console.log("Emitting " + fileName);
169+
console.log("Emitting ".concat(fileName));
170170
}
171171
else {
172-
console.log("Emitting " + fileName + " failed");
172+
console.log("Emitting ".concat(fileName, " failed"));
173173
logErrors(fileName);
174174
}
175175
output.outputFiles.forEach(function (o) {
@@ -184,10 +184,10 @@ function watch(rootFileNames, options) {
184184
var message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
185185
if (diagnostic.file) {
186186
var _a = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), line = _a.line, character = _a.character;
187-
console.log(" Error " + diagnostic.file.fileName + " (" + (line + 1) + "," + (character + 1) + "): " + message);
187+
console.log(" Error ".concat(diagnostic.file.fileName, " (").concat(line + 1, ",").concat(character + 1, "): ").concat(message));
188188
}
189189
else {
190-
console.log(" Error: " + message);
190+
console.log(" Error: ".concat(message));
191191
}
192192
});
193193
}

tests/baselines/reference/TemplateExpression1.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
var v = `foo ${ a
33

44
//// [TemplateExpression1.js]
5-
var v = "foo " + a;
5+
var v = "foo ".concat(a);

tests/baselines/reference/asOperator3.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cook
1515
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
1616
return cooked;
1717
};
18-
var a = "" + (123 + 456);
19-
var b = "leading " + (123 + 456);
20-
var c = 123 + 456 + " trailing";
21-
var d = "Hello " + 123 + " World";
18+
var a = "".concat(123 + 456);
19+
var b = "leading ".concat(123 + 456);
20+
var c = "".concat(123 + 456, " trailing");
21+
var d = "Hello ".concat(123, " World");
2222
var e = "Hello";
23-
var f = 1 + (1 + " end of string");
23+
var f = 1 + "".concat(1, " end of string");
2424
var g = tag(__makeTemplateObject(["Hello ", " World"], ["Hello ", " World"]), 123);
2525
var h = tag(__makeTemplateObject(["Hello"], ["Hello"]));

tests/baselines/reference/checkJsObjectLiteralIndexSignatures.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ stringIndex[s].toFixed();
1616
// @ts-check
1717
var _a, _b;
1818
var n = Math.random();
19-
var s = "" + n;
19+
var s = "".concat(n);
2020
var numericIndex = (_a = {}, _a[n] = 1, _a);
2121
numericIndex[n].toFixed();
2222
var stringIndex = (_b = {}, _b[s] = 1, _b);

0 commit comments

Comments
 (0)