Skip to content

Commit 94af0e5

Browse files
authored
Improve template tokenizing (#13919)
* add benchmarks * refactor: tokenize template as middle + tail * perf: avoid push tc.brace * refactor: overwrite skipSpace in jsx plugin * transform tl.templateMiddle/Tail * refactor: simplify JSX context tracking * fix flow error * refactor: move JSX context to context.js * fix: ensure comment stack is correctly handled * rename createPositionFromPosition * rename token type and methods * add tokenIsTemplate * refactor: merge babel 7 logic in babel7CompatTokens * fix flow error
1 parent 3a85ddf commit 94af0e5

File tree

22 files changed

+1006
-175
lines changed

22 files changed

+1006
-175
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Benchmark from "benchmark";
2+
import baseline from "@babel-baseline/parser";
3+
import current from "@babel/parser";
4+
import { report } from "../../util.mjs";
5+
6+
const suite = new Benchmark.Suite();
7+
function createInput(length) {
8+
return "{".repeat(length) + "0" + "}".repeat(length);
9+
}
10+
function benchCases(name, implementation, options) {
11+
for (const length of [128, 256, 512, 1024]) {
12+
const input = createInput(length);
13+
suite.add(`${name} ${length} nested template elements`, () => {
14+
implementation.parse(input, options);
15+
});
16+
}
17+
}
18+
19+
benchCases("baseline", baseline);
20+
benchCases("current", current);
21+
22+
suite.on("cycle", report).run();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Benchmark from "benchmark";
2+
import baseline from "@babel-baseline/parser";
3+
import current from "@babel/parser";
4+
import { report } from "../../util.mjs";
5+
6+
const suite = new Benchmark.Suite();
7+
function createInput(length) {
8+
return "<t a={x}>{y}".repeat(length) + "</t>".repeat(length);
9+
}
10+
function benchCases(name, implementation, options) {
11+
for (const length of [128, 256, 512, 1024]) {
12+
const input = createInput(length);
13+
suite.add(
14+
`${name} ${length} nested jsx elements with one attribute and text`,
15+
() => {
16+
implementation.parse(input, options);
17+
}
18+
);
19+
}
20+
}
21+
22+
benchCases("baseline", baseline, { plugins: ["jsx"] });
23+
benchCases("current", current, { plugins: ["jsx"] });
24+
25+
suite.on("cycle", report).run();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Benchmark from "benchmark";
2+
import baseline from "@babel-baseline/parser";
3+
import current from "@babel/parser";
4+
import { report } from "../../util.mjs";
5+
6+
const suite = new Benchmark.Suite();
7+
function createInput(length) {
8+
return "` ${".repeat(length) + "0" + "}`".repeat(length);
9+
}
10+
function benchCases(name, implementation, options) {
11+
for (const length of [128, 256, 512, 1024]) {
12+
const input = createInput(length);
13+
suite.add(`${name} ${length} nested template elements`, () => {
14+
implementation.parse(input, options);
15+
});
16+
}
17+
}
18+
19+
benchCases("baseline", baseline);
20+
benchCases("current", current);
21+
22+
suite.on("cycle", report).run();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Benchmark from "benchmark";
2+
import baseline from "@babel-baseline/parser";
3+
import current from "@babel/parser";
4+
import { report } from "../../util.mjs";
5+
6+
const suite = new Benchmark.Suite();
7+
function createInput(length) {
8+
return "`" + " ${0}".repeat(length) + "`";
9+
}
10+
function benchCases(name, implementation, options) {
11+
for (const length of [128, 256, 512, 1024]) {
12+
const input = createInput(length);
13+
suite.add(`${name} ${length} template elements`, () => {
14+
implementation.parse(input, options);
15+
});
16+
}
17+
}
18+
19+
current.parse(createInput(1));
20+
benchCases("baseline", baseline);
21+
benchCases("current", current);
22+
23+
suite.on("cycle", report).run();

eslint/babel-eslint-parser/src/convert/convertTokens.cjs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,6 @@ function convertTemplateType(tokens, tl) {
7171
templateTokens.push(token);
7272
break;
7373

74-
case tl.eof:
75-
if (curlyBrace) {
76-
result.push(curlyBrace);
77-
}
78-
79-
break;
80-
8174
default:
8275
if (curlyBrace) {
8376
result.push(curlyBrace);
@@ -186,6 +179,8 @@ function convertToken(token, source, tl) {
186179
token.value = `${token.value}n`;
187180
} else if (label === tl.privateName) {
188181
token.type = "PrivateIdentifier";
182+
} else if (label === tl.templateNonTail || label === tl.templateTail) {
183+
token.type = "Template";
189184
}
190185

191186
if (typeof token.type !== "string") {
@@ -196,22 +191,26 @@ function convertToken(token, source, tl) {
196191

197192
module.exports = function convertTokens(tokens, code, tl) {
198193
const result = [];
199-
200-
const withoutComments = convertTemplateType(tokens, tl).filter(
201-
t => t.type !== "CommentLine" && t.type !== "CommentBlock",
202-
);
203-
for (let i = 0, { length } = withoutComments; i < length; i++) {
204-
const token = withoutComments[i];
194+
const templateTypeMergedTokens = process.env.BABEL_8_BREAKING
195+
? tokens
196+
: convertTemplateType(tokens, tl);
197+
// The last token is always tt.eof and should be skipped
198+
for (let i = 0, { length } = templateTypeMergedTokens; i < length - 1; i++) {
199+
const token = templateTypeMergedTokens[i];
200+
const tokenType = token.type;
201+
if (tokenType === "CommentLine" || tokenType === "CommentBlock") {
202+
continue;
203+
}
205204

206205
if (!process.env.BABEL_8_BREAKING) {
207206
// Babel 8 already produces a single token
208207

209208
if (
210209
ESLINT_VERSION >= 8 &&
211210
i + 1 < length &&
212-
token.type.label === tl.hash
211+
tokenType.label === tl.hash
213212
) {
214-
const nextToken = withoutComments[i + 1];
213+
const nextToken = templateTypeMergedTokens[i + 1];
215214

216215
// We must disambiguate private identifier from the hack pipes topic token
217216
if (nextToken.type.label === tl.name && token.end === nextToken.start) {

packages/babel-parser/src/parser/expression.js

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
tokenIsPostfix,
2828
tokenIsPrefix,
2929
tokenIsRightAssociative,
30+
tokenIsTemplate,
3031
tokenKeywordOrIdentifierIsKeyword,
3132
tokenLabelName,
3233
tokenOperatorPrecedence,
@@ -43,7 +44,7 @@ import {
4344
isIdentifierStart,
4445
canBeReservedWord,
4546
} from "../util/identifier";
46-
import { Position } from "../util/location";
47+
import { Position, createPositionWithColumnOffset } from "../util/location";
4748
import * as charCodes from "charcodes";
4849
import {
4950
BIND_OUTSIDE,
@@ -706,9 +707,10 @@ export default class ExpressionParser extends LValParser {
706707
noCalls: ?boolean,
707708
state: N.ParseSubscriptState,
708709
): N.Expression {
709-
if (!noCalls && this.eat(tt.doubleColon)) {
710+
const { type } = this.state;
711+
if (!noCalls && type === tt.doubleColon) {
710712
return this.parseBind(base, startPos, startLoc, noCalls, state);
711-
} else if (this.match(tt.backQuote)) {
713+
} else if (tokenIsTemplate(type)) {
712714
return this.parseTaggedTemplateExpression(
713715
base,
714716
startPos,
@@ -719,7 +721,7 @@ export default class ExpressionParser extends LValParser {
719721

720722
let optional = false;
721723

722-
if (this.match(tt.questionDot)) {
724+
if (type === tt.questionDot) {
723725
if (noCalls && this.lookaheadCharCode() === charCodes.leftParenthesis) {
724726
// stop at `?.` when parsing `new a?.()`
725727
state.stop = true;
@@ -801,6 +803,7 @@ export default class ExpressionParser extends LValParser {
801803
): N.Expression {
802804
const node = this.startNodeAt(startPos, startLoc);
803805
node.object = base;
806+
this.next(); // eat '::'
804807
node.callee = this.parseNoCallExpr();
805808
state.stop = true;
806809
return this.parseSubscripts(
@@ -1153,7 +1156,8 @@ export default class ExpressionParser extends LValParser {
11531156
case tt._new:
11541157
return this.parseNewOrNewTarget();
11551158

1156-
case tt.backQuote:
1159+
case tt.templateNonTail:
1160+
case tt.templateTail:
11571161
return this.parseTemplate(false);
11581162

11591163
// BindExpression[Yield]
@@ -1832,37 +1836,47 @@ export default class ExpressionParser extends LValParser {
18321836
// Parse template expression.
18331837

18341838
parseTemplateElement(isTagged: boolean): N.TemplateElement {
1835-
const elem = this.startNode();
1836-
if (this.state.value === null) {
1839+
const { start, end, value } = this.state;
1840+
const elemStart = start + 1;
1841+
const elem = this.startNodeAt(
1842+
elemStart,
1843+
createPositionWithColumnOffset(this.state.startLoc, 1),
1844+
);
1845+
if (value === null) {
18371846
if (!isTagged) {
1838-
this.raise(this.state.start + 1, Errors.InvalidEscapeSequenceTemplate);
1847+
this.raise(start + 2, Errors.InvalidEscapeSequenceTemplate);
18391848
}
18401849
}
1850+
1851+
const isTail = this.match(tt.templateTail);
1852+
const endOffset = isTail ? -1 : -2;
1853+
const elemEnd = end + endOffset;
18411854
elem.value = {
1842-
raw: this.input
1843-
.slice(this.state.start, this.state.end)
1844-
.replace(/\r\n?/g, "\n"),
1845-
cooked: this.state.value,
1855+
raw: this.input.slice(elemStart, elemEnd).replace(/\r\n?/g, "\n"),
1856+
cooked: value === null ? null : value.slice(1, endOffset),
18461857
};
1858+
elem.tail = isTail;
18471859
this.next();
1848-
elem.tail = this.match(tt.backQuote);
1849-
return this.finishNode(elem, "TemplateElement");
1860+
this.finishNode(elem, "TemplateElement");
1861+
this.resetEndLocation(
1862+
elem,
1863+
elemEnd,
1864+
createPositionWithColumnOffset(this.state.lastTokEndLoc, endOffset),
1865+
);
1866+
return elem;
18501867
}
18511868

18521869
// https://tc39.es/ecma262/#prod-TemplateLiteral
18531870
parseTemplate(isTagged: boolean): N.TemplateLiteral {
18541871
const node = this.startNode();
1855-
this.next();
18561872
node.expressions = [];
18571873
let curElt = this.parseTemplateElement(isTagged);
18581874
node.quasis = [curElt];
18591875
while (!curElt.tail) {
1860-
this.expect(tt.dollarBraceL);
18611876
node.expressions.push(this.parseTemplateSubstitution());
1862-
this.expect(tt.braceR);
1877+
this.readTemplateContinuation();
18631878
node.quasis.push((curElt = this.parseTemplateElement(isTagged)));
18641879
}
1865-
this.next();
18661880
return this.finishNode(node, "TemplateLiteral");
18671881
}
18681882

@@ -2681,21 +2695,22 @@ export default class ExpressionParser extends LValParser {
26812695
}
26822696

26832697
isAmbiguousAwait(): boolean {
2698+
if (this.hasPrecedingLineBreak()) return true;
2699+
const { type } = this.state;
26842700
return (
2685-
this.hasPrecedingLineBreak() ||
26862701
// All the following expressions are ambiguous:
26872702
// await + 0, await - 0, await ( 0 ), await [ 0 ], await / 0 /u, await ``
2688-
this.match(tt.plusMin) ||
2689-
this.match(tt.parenL) ||
2690-
this.match(tt.bracketL) ||
2691-
this.match(tt.backQuote) ||
2703+
type === tt.plusMin ||
2704+
type === tt.parenL ||
2705+
type === tt.bracketL ||
2706+
tokenIsTemplate(type) ||
26922707
// Sometimes the tokenizer generates tt.slash for regexps, and this is
26932708
// handler by parseExprAtom
2694-
this.match(tt.regexp) ||
2695-
this.match(tt.slash) ||
2709+
type === tt.regexp ||
2710+
type === tt.slash ||
26962711
// This code could be parsed both as a modulo operator or as an intrinsic:
26972712
// await %x(0)
2698-
(this.hasPlugin("v8intrinsic") && this.match(tt.modulo))
2713+
(this.hasPlugin("v8intrinsic") && type === tt.modulo)
26992714
);
27002715
}
27012716

0 commit comments

Comments
 (0)