Skip to content

Commit 344d505

Browse files
committed
add support for Lift Template Literal Restriction
1 parent 6e0442e commit 344d505

File tree

11 files changed

+396
-24
lines changed

11 files changed

+396
-24
lines changed

src/compiler/parser.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,8 +1034,12 @@ namespace ts {
10341034
return currentToken = scanner.reScanSlashToken();
10351035
}
10361036

1037-
function reScanTemplateToken(): SyntaxKind {
1038-
return currentToken = scanner.reScanTemplateToken();
1037+
function reScanTemplateToken(isTaggedTemplate?: boolean): SyntaxKind {
1038+
return currentToken = scanner.reScanTemplateToken(isTaggedTemplate);
1039+
}
1040+
1041+
function reScanTemplateHead(): SyntaxKind {
1042+
return currentToken = scanner.reScanTemplateHead();
10391043
}
10401044

10411045
function scanJsxIdentifier(): SyntaxKind {
@@ -2104,17 +2108,17 @@ namespace ts {
21042108
return allowIdentifierNames ? parseIdentifierName() : parseIdentifier();
21052109
}
21062110

2107-
function parseTemplateExpression(): TemplateExpression {
2111+
function parseTemplateExpression(isTaggedTemplate?: boolean): TemplateExpression {
21082112
const template = <TemplateExpression>createNode(SyntaxKind.TemplateExpression);
21092113

2110-
template.head = parseTemplateHead();
2114+
template.head = parseTemplateHead(isTaggedTemplate);
21112115
Debug.assert(template.head.kind === SyntaxKind.TemplateHead, "Template head has wrong token kind");
21122116

21132117
const list = [];
21142118
const listPos = getNodePos();
21152119

21162120
do {
2117-
list.push(parseTemplateSpan());
2121+
list.push(parseTemplateSpan(isTaggedTemplate));
21182122
}
21192123
while (lastOrUndefined(list).literal.kind === SyntaxKind.TemplateMiddle);
21202124

@@ -2123,13 +2127,13 @@ namespace ts {
21232127
return finishNode(template);
21242128
}
21252129

2126-
function parseTemplateSpan(): TemplateSpan {
2130+
function parseTemplateSpan(isTaggedTemplate?: boolean): TemplateSpan {
21272131
const span = <TemplateSpan>createNode(SyntaxKind.TemplateSpan);
21282132
span.expression = allowInAnd(parseExpression);
21292133

21302134
let literal: TemplateMiddle | TemplateTail;
21312135
if (token() === SyntaxKind.CloseBraceToken) {
2132-
reScanTemplateToken();
2136+
reScanTemplateToken(isTaggedTemplate);
21332137
literal = parseTemplateMiddleOrTemplateTail();
21342138
}
21352139
else {
@@ -2144,7 +2148,10 @@ namespace ts {
21442148
return <LiteralExpression>parseLiteralLikeNode(token());
21452149
}
21462150

2147-
function parseTemplateHead(): TemplateHead {
2151+
function parseTemplateHead(isTaggedTemplate?: boolean): TemplateHead {
2152+
if (isTaggedTemplate) {
2153+
reScanTemplateHead();
2154+
}
21482155
const fragment = parseLiteralLikeNode(token());
21492156
Debug.assert(fragment.kind === SyntaxKind.TemplateHead, "Template head has wrong token kind");
21502157
return <TemplateHead>fragment;
@@ -2157,7 +2164,7 @@ namespace ts {
21572164
}
21582165

21592166
function parseLiteralLikeNode(kind: SyntaxKind): LiteralExpression | LiteralLikeNode {
2160-
const node = <LiteralExpression>createNode(kind);
2167+
const node = <LiteralExpression | LiteralLikeNode>createNode(kind);
21612168
const text = scanner.getTokenValue();
21622169
node.text = text;
21632170

@@ -2179,6 +2186,10 @@ namespace ts {
21792186
(<NumericLiteral>node).numericLiteralFlags = scanner.getTokenFlags() & TokenFlags.NumericLiteralFlags;
21802187
}
21812188

2189+
if (node.kind === SyntaxKind.TemplateHead || node.kind === SyntaxKind.TemplateMiddle || node.kind === SyntaxKind.TemplateTail) {
2190+
(<TemplateHead | TemplateMiddle | TemplateTail>node).notEscapeFlags = scanner.getTokenFlags() & TokenFlags.NotEscape;
2191+
}
2192+
21822193
nextToken();
21832194
finishNode(node);
21842195

@@ -4414,7 +4425,7 @@ namespace ts {
44144425
tagExpression.typeArguments = typeArguments;
44154426
tagExpression.template = token() === SyntaxKind.NoSubstitutionTemplateLiteral
44164427
? <NoSubstitutionTemplateLiteral>parseLiteralNode()
4417-
: parseTemplateExpression();
4428+
: parseTemplateExpression(/*isTaggedTemplate*/ true);
44184429
return finishNode(tagExpression);
44194430
}
44204431

src/compiler/scanner.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ namespace ts {
2727
getTokenFlags(): TokenFlags;
2828
reScanGreaterToken(): SyntaxKind;
2929
reScanSlashToken(): SyntaxKind;
30-
reScanTemplateToken(): SyntaxKind;
30+
reScanTemplateToken(isTaggedTemplate?: boolean): SyntaxKind;
31+
reScanTemplateHead(): SyntaxKind;
3132
scanJsxIdentifier(): SyntaxKind;
3233
scanJsxAttributeValue(): SyntaxKind;
3334
reScanJsxToken(): JsxTokenSyntaxKind;
@@ -428,6 +429,16 @@ namespace ts {
428429
return ch >= CharacterCodes._0 && ch <= CharacterCodes._7;
429430
}
430431

432+
/* @internal */
433+
export function isHexDigit(ch: number): boolean {
434+
return isDigit(ch) || ch >= CharacterCodes.A && ch <= CharacterCodes.F || ch >= CharacterCodes.a && ch <= CharacterCodes.f;
435+
}
436+
437+
/* @internal */
438+
export function isCodePoint(code: number): boolean {
439+
return code <= 0x10FFFF;
440+
}
441+
431442
export function couldStartTrivia(text: string, pos: number): boolean {
432443
// Keep in sync with skipTrivia
433444
const ch = text.charCodeAt(pos);
@@ -838,6 +849,7 @@ namespace ts {
838849
reScanGreaterToken,
839850
reScanSlashToken,
840851
reScanTemplateToken,
852+
reScanTemplateHead,
841853
scanJsxIdentifier,
842854
scanJsxAttributeValue,
843855
reScanJsxToken,
@@ -1054,7 +1066,7 @@ namespace ts {
10541066
* Sets the current 'tokenValue' and returns a NoSubstitutionTemplateLiteral or
10551067
* a literal component of a TemplateExpression.
10561068
*/
1057-
function scanTemplateAndSetTokenValue(): SyntaxKind {
1069+
function scanTemplateAndSetTokenValue(isTaggedTemplate?: boolean): SyntaxKind {
10581070
const startedWithBacktick = text.charCodeAt(pos) === CharacterCodes.backtick;
10591071

10601072
pos++;
@@ -1092,7 +1104,7 @@ namespace ts {
10921104
// Escape character
10931105
if (currChar === CharacterCodes.backslash) {
10941106
contents += text.substring(start, pos);
1095-
contents += scanEscapeSequence();
1107+
contents += scanEscapeSequence(isTaggedTemplate);
10961108
start = pos;
10971109
continue;
10981110
}
@@ -1121,7 +1133,8 @@ namespace ts {
11211133
return resultingToken;
11221134
}
11231135

1124-
function scanEscapeSequence(): string {
1136+
function scanEscapeSequence(isTaggedTemplate?: boolean): string {
1137+
const start = pos;
11251138
pos++;
11261139
if (pos >= end) {
11271140
error(Diagnostics.Unexpected_end_of_text);
@@ -1131,6 +1144,11 @@ namespace ts {
11311144
pos++;
11321145
switch (ch) {
11331146
case CharacterCodes._0:
1147+
if (isTaggedTemplate && isDigit(text.charCodeAt(pos))) {
1148+
pos++;
1149+
tokenFlags |= TokenFlags.NotEscape;
1150+
return text.substring(start, pos);
1151+
}
11341152
return "\0";
11351153
case CharacterCodes.b:
11361154
return "\b";
@@ -1149,17 +1167,55 @@ namespace ts {
11491167
case CharacterCodes.doubleQuote:
11501168
return "\"";
11511169
case CharacterCodes.u:
1170+
if (isTaggedTemplate) {
1171+
if (pos < end && !isHexDigit(text.charCodeAt(pos)) && text.charCodeAt(pos) !== CharacterCodes.openBrace) {
1172+
tokenFlags |= TokenFlags.NotEscape;
1173+
return "u";
1174+
}
1175+
1176+
for (let i = 0; i < 3; i++) {
1177+
if (pos + i + 1 < end && isHexDigit(text.charCodeAt(pos + i)) && !isHexDigit(text.charCodeAt(pos + i + 1)) && text.charCodeAt(pos + i + 1) !== CharacterCodes.openBrace) {
1178+
pos += i;
1179+
tokenFlags |= TokenFlags.NotEscape;
1180+
return text.substring(start, pos);
1181+
}
1182+
}
1183+
}
11521184
// '\u{DDDDDDDD}'
11531185
if (pos < end && text.charCodeAt(pos) === CharacterCodes.openBrace) {
1154-
tokenFlags |= TokenFlags.ExtendedUnicodeEscape;
11551186
pos++;
1156-
return scanExtendedUnicodeEscape();
1187+
1188+
if (isTaggedTemplate && !isHexDigit(text.charCodeAt(pos))) {
1189+
tokenFlags |= TokenFlags.NotEscape;
1190+
return "u{";
1191+
}
1192+
1193+
const escapedValue = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false);
1194+
if (isTaggedTemplate) {
1195+
if (!isCodePoint(escapedValue) || text.charCodeAt(pos) !== CharacterCodes.closeBrace) {
1196+
tokenFlags |= TokenFlags.NotEscape;
1197+
return text.substring(start, pos);
1198+
}
1199+
}
1200+
tokenFlags |= TokenFlags.ExtendedUnicodeEscape;
1201+
return scanExtendedUnicodeEscape(escapedValue);
11571202
}
11581203

11591204
// '\uDDDD'
11601205
return scanHexadecimalEscape(/*numDigits*/ 4);
11611206

11621207
case CharacterCodes.x:
1208+
if (isTaggedTemplate) {
1209+
if (!isHexDigit(text.charCodeAt(pos))) {
1210+
tokenFlags |= TokenFlags.NotEscape;
1211+
return "x";
1212+
}
1213+
else if (!isHexDigit(text.charCodeAt(pos + 1))) {
1214+
pos++;
1215+
tokenFlags |= TokenFlags.NotEscape;
1216+
return "x" + String.fromCharCode(text.charCodeAt(pos));
1217+
}
1218+
}
11631219
// '\xDD'
11641220
return scanHexadecimalEscape(/*numDigits*/ 2);
11651221

@@ -1191,8 +1247,7 @@ namespace ts {
11911247
}
11921248
}
11931249

1194-
function scanExtendedUnicodeEscape(): string {
1195-
const escapedValue = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false);
1250+
function scanExtendedUnicodeEscape(escapedValue: number): string {
11961251
let isInvalidExtendedEscape = false;
11971252

11981253
// Validate the value of the digit
@@ -1825,10 +1880,15 @@ namespace ts {
18251880
/**
18261881
* Unconditionally back up and scan a template expression portion.
18271882
*/
1828-
function reScanTemplateToken(): SyntaxKind {
1883+
function reScanTemplateToken(isTaggedTemplate?: boolean): SyntaxKind {
18291884
Debug.assert(token === SyntaxKind.CloseBraceToken, "'reScanTemplateToken' should only be called on a '}'");
18301885
pos = tokenPos;
1831-
return token = scanTemplateAndSetTokenValue();
1886+
return token = scanTemplateAndSetTokenValue(isTaggedTemplate);
1887+
}
1888+
1889+
function reScanTemplateHead(): SyntaxKind {
1890+
pos = tokenPos;
1891+
return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ true);
18321892
}
18331893

18341894
function reScanJsxToken(): JsxTokenSyntaxKind {

src/compiler/transformers/es2015.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3656,10 +3656,10 @@ namespace ts {
36563656
rawStrings.push(getRawLiteral(template));
36573657
}
36583658
else {
3659-
cookedStrings.push(createLiteral(template.head.text));
3659+
cookedStrings.push(template.head.notEscapeFlags ? createIdentifier("undefined") : createLiteral(template.head.text));
36603660
rawStrings.push(getRawLiteral(template.head));
36613661
for (const templateSpan of template.templateSpans) {
3662-
cookedStrings.push(createLiteral(templateSpan.literal.text));
3662+
cookedStrings.push(templateSpan.literal.notEscapeFlags ? createIdentifier("undefined") : createLiteral(templateSpan.literal.text));
36633663
rawStrings.push(getRawLiteral(templateSpan.literal));
36643664
templateArguments.push(visitNode(templateSpan.expression, visitor, isExpression));
36653665
}

src/compiler/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,6 +1593,7 @@ namespace ts {
15931593
BinarySpecifier = 1 << 7, // e.g. `0b0110010000000000`
15941594
OctalSpecifier = 1 << 8, // e.g. `0o777`
15951595
ContainsSeparator = 1 << 9, // e.g. `0b1100_0101`
1596+
NotEscape = 1 << 10, // e.g. `\uhello`
15961597
BinaryOrOctalSpecifier = BinarySpecifier | OctalSpecifier,
15971598
NumericLiteralFlags = Scientific | Octal | HexSpecifier | BinarySpecifier | OctalSpecifier | ContainsSeparator
15981599
}
@@ -1606,16 +1607,22 @@ namespace ts {
16061607
export interface TemplateHead extends LiteralLikeNode {
16071608
kind: SyntaxKind.TemplateHead;
16081609
parent?: TemplateExpression;
1610+
/* @internal */
1611+
notEscapeFlags?: TokenFlags;
16091612
}
16101613

16111614
export interface TemplateMiddle extends LiteralLikeNode {
16121615
kind: SyntaxKind.TemplateMiddle;
16131616
parent?: TemplateSpan;
1617+
/* @internal */
1618+
notEscapeFlags?: TokenFlags;
16141619
}
16151620

16161621
export interface TemplateTail extends LiteralLikeNode {
16171622
kind: SyntaxKind.TemplateTail;
16181623
parent?: TemplateSpan;
1624+
/* @internal */
1625+
notEscapeFlags?: TokenFlags;
16191626
}
16201627

16211628
export type TemplateLiteral = TemplateExpression | NoSubstitutionTemplateLiteral;

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2953,7 +2953,8 @@ declare namespace ts {
29532953
isUnterminated(): boolean;
29542954
reScanGreaterToken(): SyntaxKind;
29552955
reScanSlashToken(): SyntaxKind;
2956-
reScanTemplateToken(): SyntaxKind;
2956+
reScanTemplateToken(isTaggedTemplate?: boolean): SyntaxKind;
2957+
reScanTemplateHead(): SyntaxKind;
29572958
scanJsxIdentifier(): SyntaxKind;
29582959
scanJsxAttributeValue(): SyntaxKind;
29592960
reScanJsxToken(): JsxTokenSyntaxKind;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2953,7 +2953,8 @@ declare namespace ts {
29532953
isUnterminated(): boolean;
29542954
reScanGreaterToken(): SyntaxKind;
29552955
reScanSlashToken(): SyntaxKind;
2956-
reScanTemplateToken(): SyntaxKind;
2956+
reScanTemplateToken(isTaggedTemplate?: boolean): SyntaxKind;
2957+
reScanTemplateHead(): SyntaxKind;
29572958
scanJsxIdentifier(): SyntaxKind;
29582959
scanJsxAttributeValue(): SyntaxKind;
29592960
reScanJsxToken(): JsxTokenSyntaxKind;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
tests/cases/conformance/es2018/template.ts(5,18): error TS1125: Hexadecimal digit expected.
2+
tests/cases/conformance/es2018/template.ts(6,15): error TS1125: Hexadecimal digit expected.
3+
tests/cases/conformance/es2018/template.ts(6,33): error TS1125: Hexadecimal digit expected.
4+
tests/cases/conformance/es2018/template.ts(6,75): error TS1125: Hexadecimal digit expected.
5+
6+
7+
==== tests/cases/conformance/es2018/template.ts (4 errors) ====
8+
function tag (str: any, ...args: any[]): string {
9+
return str
10+
}
11+
12+
const x = tag`\u{hello} ${ 100 } \xtraordinary ${ 200 } wonderful ${ 300 } \uworld`;
13+
14+
!!! error TS1125: Hexadecimal digit expected.
15+
const y = `\u{hello} ${ 100 } \xtraordinary ${ 200 } wonderful ${ 300 } \uworld`;
16+
17+
!!! error TS1125: Hexadecimal digit expected.
18+
19+
!!! error TS1125: Hexadecimal digit expected.
20+
21+
!!! error TS1125: Hexadecimal digit expected.
22+
23+
const a1 = tag`${ 100 }\0` // \0
24+
const a2 = tag`${ 100 }\00` // \\00
25+
const a3 = tag`${ 100 }\u` // \\u
26+
const a4 = tag`${ 100 }\u0` // \\u0
27+
const a5 = tag`${ 100 }\u00` // \\u00
28+
const a6 = tag`${ 100 }\u000` // \\u000
29+
const a7 = tag`${ 100 }\u0000` // \u0000
30+
const a8 = tag`${ 100 }\u{` // \\u{
31+
const a9 = tag`${ 100 }\u{10FFFF` // \\u{10FFFF
32+
const a10 = tag`${ 100 }\u{1d306` // \\u{1d306
33+
const a11 = tag`${ 100 }\u{1d306}` // \u{1d306}
34+
const a12 = tag`${ 100 }\x` // \\x
35+
const a13 = tag`${ 100 }\x0` // \\x0
36+

0 commit comments

Comments
 (0)