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

+12-77
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

+1
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",
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

+2-2
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

+1-1
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

+1-1
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

+4-4
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

+1-1
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

+5-5
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

+1-1
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);

tests/baselines/reference/classAttributeInferenceTemplate.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ var MyClass = /** @class */ (function () {
1919
function MyClass() {
2020
var variable = 'something';
2121
this.property = "foo"; // Correctly inferred as `string`
22-
this.property2 = "foo-" + variable; // Causes an error
23-
var localProperty = "foo-" + variable; // Correctly inferred as `string`
22+
this.property2 = "foo-".concat(variable); // Causes an error
23+
var localProperty = "foo-".concat(variable); // Correctly inferred as `string`
2424
}
2525
return MyClass;
2626
}());

tests/baselines/reference/computedPropertyNames10_ES5.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ var v = (_a = {},
3232
_a[a] = function () { },
3333
_a[true] = function () { },
3434
_a["hello bye"] = function () { },
35-
_a["hello " + a + " bye"] = function () { },
35+
_a["hello ".concat(a, " bye")] = function () { },
3636
_a);

tests/baselines/reference/computedPropertyNames11_ES5.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ var v = (_a = {},
7272
enumerable: false,
7373
configurable: true
7474
}),
75-
Object.defineProperty(_a, "hello " + a + " bye", {
75+
Object.defineProperty(_a, "hello ".concat(a, " bye"), {
7676
get: function () { return 0; },
7777
enumerable: false,
7878
configurable: true

tests/baselines/reference/computedPropertyNames12_ES5.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ var C = /** @class */ (function () {
2727
this["hello bye"] = 0;
2828
}
2929
var _a, _b, _c;
30-
_a = n, s + s, _b = s + n, +s, _c = "hello " + a + " bye";
30+
_a = n, s + s, _b = s + n, +s, _c = "hello ".concat(a, " bye");
3131
C[_c] = 0;
3232
return C;
3333
}());

tests/baselines/reference/computedPropertyNames13_ES5.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ var C = /** @class */ (function () {
3333
C.prototype[a] = function () { };
3434
C[true] = function () { };
3535
C.prototype["hello bye"] = function () { };
36-
C["hello " + a + " bye"] = function () { };
36+
C["hello ".concat(a, " bye")] = function () { };
3737
return C;
3838
}());

tests/baselines/reference/computedPropertyNames16_ES5.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ var C = /** @class */ (function () {
7373
enumerable: false,
7474
configurable: true
7575
});
76-
Object.defineProperty(C.prototype, "hello " + a + " bye", {
76+
Object.defineProperty(C.prototype, "hello ".concat(a, " bye"), {
7777
get: function () { return 0; },
7878
enumerable: false,
7979
configurable: true

tests/baselines/reference/computedPropertyNames4_ES5.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ var v = (_a = {},
3232
_a[a] = 1,
3333
_a[true] = 0,
3434
_a["hello bye"] = 0,
35-
_a["hello " + a + " bye"] = 0,
35+
_a["hello ".concat(a, " bye")] = 0,
3636
_a);

tests/baselines/reference/constEnumErrors.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ var E;
5252
var y0 = E2[1];
5353
var name = "A";
5454
var y1 = E2[name];
55-
var y2 = E2["" + name];
55+
var y2 = E2["".concat(name)];
5656
var x = E2;
5757
var y = [E2];
5858
function foo(t) {

tests/baselines/reference/declarationNoDanglingGenerics.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ exports.CKind = exports.BKind = exports.AKind = void 0;
5454
var kindCache = {};
5555
function register(kind) {
5656
if (kindCache[kind]) {
57-
throw new Error("Class with kind \"" + kind + "\" is already registered.");
57+
throw new Error("Class with kind \"".concat(kind, "\" is already registered."));
5858
}
5959
kindCache[kind] = true;
6060
}

tests/baselines/reference/destructuringInitializerContextualTypeFromContext.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ var Parent = function (_a) {
5555
};
5656
var Child = function (_a) {
5757
var children = _a.children, _b = _a.name, name = _b === void 0 ? "Artemis" : _b, props = __rest(_a, ["children", "name"]);
58-
return "name: " + name + " props: " + JSON.stringify(props);
58+
return "name: ".concat(name, " props: ").concat(JSON.stringify(props));
5959
};
6060
f(function (_a) {
6161
var _1 = _a[0], _b = _a[1], _2 = _b === void 0 ? undefined : _b;

tests/baselines/reference/emitExponentiationOperatorInTempalteString4.js

+17-17
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,22 @@ var t1 = 10;
3131
var t2 = 10;
3232
var s;
3333
// With TemplateTail
34-
Math.pow(t1, -t2) + " world";
35-
Math.pow((-t1), t2) - t1 + " world";
36-
Math.pow((-++t1), t2) - t1 + " world";
37-
Math.pow((-t1++), t2) - t1 + " world";
38-
Math.pow((~t1), Math.pow(t2, --t1)) + " world";
39-
typeof (Math.pow(t1, Math.pow(t2, t1))) + " world";
34+
"".concat(Math.pow(t1, -t2), " world");
35+
"".concat(Math.pow((-t1), t2) - t1, " world");
36+
"".concat(Math.pow((-++t1), t2) - t1, " world");
37+
"".concat(Math.pow((-t1++), t2) - t1, " world");
38+
"".concat(Math.pow((~t1), Math.pow(t2, --t1)), " world");
39+
"".concat(typeof (Math.pow(t1, Math.pow(t2, t1))), " world");
4040
// TempateHead & TemplateTail are empt
41-
Math.pow(t1, -t2) + " hello world " + Math.pow(t1, -t2);
42-
Math.pow((-t1), t2) - t1 + " hello world " + (Math.pow((-t1), t2) - t1);
43-
Math.pow((-++t1), t2) - t1 + " hello world " + Math.pow(t1, Math.pow((-++t1), -t1));
44-
Math.pow((-t1++), t2) - t1 + " hello world " + Math.pow(t2, Math.pow((-t1++), -t1));
45-
Math.pow((~t1), Math.pow(t2, --t1)) + " hello world " + Math.pow((~t1), Math.pow(t2, --t1));
46-
typeof (Math.pow(t1, Math.pow(t2, t1))) + " hello world " + typeof (Math.pow(t1, Math.pow(t2, t1)));
41+
"".concat(Math.pow(t1, -t2), " hello world ").concat(Math.pow(t1, -t2));
42+
"".concat(Math.pow((-t1), t2) - t1, " hello world ").concat(Math.pow((-t1), t2) - t1);
43+
"".concat(Math.pow((-++t1), t2) - t1, " hello world ").concat(Math.pow(t1, Math.pow((-++t1), -t1)));
44+
"".concat(Math.pow((-t1++), t2) - t1, " hello world ").concat(Math.pow(t2, Math.pow((-t1++), -t1)));
45+
"".concat(Math.pow((~t1), Math.pow(t2, --t1)), " hello world ").concat(Math.pow((~t1), Math.pow(t2, --t1)));
46+
"".concat(typeof (Math.pow(t1, Math.pow(t2, t1))), " hello world ").concat(typeof (Math.pow(t1, Math.pow(t2, t1))));
4747
// With templateHead
48-
"hello " + (Math.pow((-t1), t2) - t1);
49-
"hello " + (Math.pow((-++t1), t2) - t1);
50-
"hello " + (Math.pow((-t1++), t2) - t1);
51-
"hello " + Math.pow((~t1), Math.pow(t2, --t1));
52-
"hello " + typeof (Math.pow(t1, Math.pow(t2, t1)));
48+
"hello ".concat(Math.pow((-t1), t2) - t1);
49+
"hello ".concat(Math.pow((-++t1), t2) - t1);
50+
"hello ".concat(Math.pow((-t1++), t2) - t1);
51+
"hello ".concat(Math.pow((~t1), Math.pow(t2, --t1)));
52+
"hello ".concat(typeof (Math.pow(t1, Math.pow(t2, t1))));

0 commit comments

Comments
 (0)