Skip to content

Commit fab343e

Browse files
bakkothzoo
authored andcommitted
Add support for invalid escapes in tagged templates (babel#274)
Per the stage-3 TC39 proposal: https://github.com/tc39/proposal-template-literal-revision
1 parent 0811438 commit fab343e

File tree

290 files changed

+11491
-38
lines changed

Some content is hidden

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

290 files changed

+11491
-38
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,4 @@ require("babylon").parse("code", {
131131
- `functionBind`
132132
- `functionSent`
133133
- `dynamicImport`
134+
- `templateInvalidEscapes`

ast/spec.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,7 @@ interface TemplateElement <: Node {
959959
type: "TemplateElement";
960960
tail: boolean;
961961
value: {
962-
cooked: string;
962+
cooked: string | null;
963963
raw: string;
964964
};
965965
}

src/parser/expression.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ pp.parseSubscripts = function (base, startPos, startLoc, noCalls) {
317317
} else if (this.match(tt.backQuote)) {
318318
const node = this.startNodeAt(startPos, startLoc);
319319
node.tag = base;
320-
node.quasi = this.parseTemplate();
320+
node.quasi = this.parseTemplate(true);
321321
base = this.finishNode(node, "TaggedTemplateExpression");
322322
} else {
323323
return base;
@@ -506,7 +506,7 @@ pp.parseExprAtom = function (refShorthandDefaultPos) {
506506
return this.parseNew();
507507

508508
case tt.backQuote:
509-
return this.parseTemplate();
509+
return this.parseTemplate(false);
510510

511511
case tt.doubleColon:
512512
node = this.startNode();
@@ -685,8 +685,15 @@ pp.parseNew = function () {
685685

686686
// Parse template expression.
687687

688-
pp.parseTemplateElement = function () {
688+
pp.parseTemplateElement = function (isTagged) {
689689
const elem = this.startNode();
690+
if (this.state.value === null) {
691+
if (!isTagged || !this.hasPlugin("templateInvalidEscapes")) {
692+
this.raise(this.state.invalidTemplateEscapePosition, "Invalid escape sequence in template");
693+
} else {
694+
this.state.invalidTemplateEscapePosition = null;
695+
}
696+
}
690697
elem.value = {
691698
raw: this.input.slice(this.state.start, this.state.end).replace(/\r\n?/g, "\n"),
692699
cooked: this.state.value
@@ -696,17 +703,17 @@ pp.parseTemplateElement = function () {
696703
return this.finishNode(elem, "TemplateElement");
697704
};
698705

699-
pp.parseTemplate = function () {
706+
pp.parseTemplate = function (isTagged) {
700707
const node = this.startNode();
701708
this.next();
702709
node.expressions = [];
703-
let curElt = this.parseTemplateElement();
710+
let curElt = this.parseTemplateElement(isTagged);
704711
node.quasis = [curElt];
705712
while (!curElt.tail) {
706713
this.expect(tt.dollarBraceL);
707714
node.expressions.push(this.parseExpression());
708715
this.expect(tt.braceR);
709-
node.quasis.push(curElt = this.parseTemplateElement());
716+
node.quasis.push(curElt = this.parseTemplateElement(isTagged));
710717
}
711718
this.next();
712719
return this.finishNode(node, "TemplateLiteral");

src/tokenizer/index.js

+52-19
Original file line numberDiff line numberDiff line change
@@ -599,17 +599,26 @@ export default class Tokenizer {
599599

600600
// Read a string value, interpreting backslash-escapes.
601601

602-
readCodePoint() {
602+
readCodePoint(throwOnInvalid) {
603603
const ch = this.input.charCodeAt(this.state.pos);
604604
let code;
605605

606-
if (ch === 123) {
606+
if (ch === 123) { // '{'
607607
const codePos = ++this.state.pos;
608-
code = this.readHexChar(this.input.indexOf("}", this.state.pos) - this.state.pos);
608+
code = this.readHexChar(this.input.indexOf("}", this.state.pos) - this.state.pos, throwOnInvalid);
609609
++this.state.pos;
610-
if (code > 0x10FFFF) this.raise(codePos, "Code point out of bounds");
610+
if (code === null) {
611+
--this.state.invalidTemplateEscapePosition; // to point to the '\'' instead of the 'u'
612+
} else if (code > 0x10FFFF) {
613+
if (throwOnInvalid) {
614+
this.raise(codePos, "Code point out of bounds");
615+
} else {
616+
this.state.invalidTemplateEscapePosition = codePos - 2;
617+
return null;
618+
}
619+
}
611620
} else {
612-
code = this.readHexChar(4);
621+
code = this.readHexChar(4, throwOnInvalid);
613622
}
614623
return code;
615624
}
@@ -636,7 +645,7 @@ export default class Tokenizer {
636645
// Reads template string tokens.
637646

638647
readTmplToken() {
639-
let out = "", chunkStart = this.state.pos;
648+
let out = "", chunkStart = this.state.pos, containsInvalid = false;
640649
for (;;) {
641650
if (this.state.pos >= this.input.length) this.raise(this.state.start, "Unterminated template");
642651
const ch = this.input.charCodeAt(this.state.pos);
@@ -651,11 +660,16 @@ export default class Tokenizer {
651660
}
652661
}
653662
out += this.input.slice(chunkStart, this.state.pos);
654-
return this.finishToken(tt.template, out);
663+
return this.finishToken(tt.template, containsInvalid ? null : out);
655664
}
656665
if (ch === 92) { // '\'
657666
out += this.input.slice(chunkStart, this.state.pos);
658-
out += this.readEscapedChar(true);
667+
const escaped = this.readEscapedChar(true);
668+
if (escaped === null) {
669+
containsInvalid = true;
670+
} else {
671+
out += escaped;
672+
}
659673
chunkStart = this.state.pos;
660674
} else if (isNewLine(ch)) {
661675
out += this.input.slice(chunkStart, this.state.pos);
@@ -682,13 +696,20 @@ export default class Tokenizer {
682696
// Used to read escaped characters
683697

684698
readEscapedChar(inTemplate) {
699+
const throwOnInvalid = !inTemplate;
685700
const ch = this.input.charCodeAt(++this.state.pos);
686701
++this.state.pos;
687702
switch (ch) {
688703
case 110: return "\n"; // 'n' -> '\n'
689704
case 114: return "\r"; // 'r' -> '\r'
690-
case 120: return String.fromCharCode(this.readHexChar(2)); // 'x'
691-
case 117: return codePointToString(this.readCodePoint()); // 'u'
705+
case 120: { // 'x'
706+
const code = this.readHexChar(2, throwOnInvalid);
707+
return code === null ? null : String.fromCharCode(code);
708+
}
709+
case 117: { // 'u'
710+
const code = this.readCodePoint(throwOnInvalid);
711+
return code === null ? null : codePointToString(code);
712+
}
692713
case 116: return "\t"; // 't' -> '\t'
693714
case 98: return "\b"; // 'b' -> '\b'
694715
case 118: return "\u000b"; // 'v' -> '\u000b'
@@ -700,19 +721,24 @@ export default class Tokenizer {
700721
return "";
701722
default:
702723
if (ch >= 48 && ch <= 55) {
724+
const codePos = this.state.pos - 1;
703725
let octalStr = this.input.substr(this.state.pos - 1, 3).match(/^[0-7]+/)[0];
704726
let octal = parseInt(octalStr, 8);
705727
if (octal > 255) {
706728
octalStr = octalStr.slice(0, -1);
707729
octal = parseInt(octalStr, 8);
708730
}
709731
if (octal > 0) {
710-
if (!this.state.containsOctal) {
732+
if (inTemplate) {
733+
this.state.invalidTemplateEscapePosition = codePos;
734+
return null;
735+
} else if (this.state.strict) {
736+
this.raise(codePos, "Octal literal in strict mode");
737+
} else if (!this.state.containsOctal) {
738+
// These properties are only used to throw an error for an octal which occurs
739+
// in a directive which occurs prior to a "use strict" directive.
711740
this.state.containsOctal = true;
712-
this.state.octalPosition = this.state.pos - 2;
713-
}
714-
if (this.state.strict || inTemplate) {
715-
this.raise(this.state.pos - 2, "Octal literal in strict mode");
741+
this.state.octalPosition = codePos;
716742
}
717743
}
718744
this.state.pos += octalStr.length - 1;
@@ -722,12 +748,19 @@ export default class Tokenizer {
722748
}
723749
}
724750

725-
// Used to read character escape sequences ('\x', '\u', '\U').
751+
// Used to read character escape sequences ('\x', '\u').
726752

727-
readHexChar(len) {
753+
readHexChar(len, throwOnInvalid) {
728754
const codePos = this.state.pos;
729755
const n = this.readInt(16, len);
730-
if (n === null) this.raise(codePos, "Bad character escape sequence");
756+
if (n === null) {
757+
if (throwOnInvalid) {
758+
this.raise(codePos, "Bad character escape sequence");
759+
} else {
760+
this.state.pos = codePos - 1;
761+
this.state.invalidTemplateEscapePosition = codePos - 1;
762+
}
763+
}
731764
return n;
732765
}
733766

@@ -755,7 +788,7 @@ export default class Tokenizer {
755788
}
756789

757790
++this.state.pos;
758-
const esc = this.readCodePoint();
791+
const esc = this.readCodePoint(true);
759792
if (!(first ? isIdentifierStart : isIdentifierChar)(esc, true)) {
760793
this.raise(escStart, "Invalid Unicode escape");
761794
}

src/tokenizer/state.js

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export default class State {
5050
this.containsEsc = this.containsOctal = false;
5151
this.octalPosition = null;
5252

53+
this.invalidTemplateEscapePosition = null;
54+
5355
this.exportedIdentifiers = [];
5456

5557
return this;
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:34)"
2+
"throws": "Octal literal in strict mode (1:35)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:37)"
2+
"throws": "Octal literal in strict mode (1:38)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:68)"
2+
"throws": "Octal literal in strict mode (1:69)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:22)"
2+
"throws": "Invalid escape sequence in template (1:23)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:1)"
2+
"throws": "Invalid escape sequence in template (1:2)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:1)"
2+
"throws": "Invalid escape sequence in template (1:2)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:1)"
2+
"throws": "Octal literal in strict mode (1:2)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:34)"
2+
"throws": "Octal literal in strict mode (1:35)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:37)"
2+
"throws": "Octal literal in strict mode (1:38)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:35)"
2+
"throws": "Octal literal in strict mode (1:36)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:35)"
2+
"throws": "Octal literal in strict mode (1:36)"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"throws": "Octal literal in strict mode (1:68)"
2+
"throws": "Octal literal in strict mode (1:69)"
33
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sampleTag`\01`

0 commit comments

Comments
 (0)