Skip to content

Commit

Permalink
Refactor private name tokenizing (#13256)
Browse files Browse the repository at this point in the history
* add benchmark

* refactor: create tt.privateName token for private names

* add backward compat privateName = hash + name to Babel 7

* perf: get private name SV from token value

* chore: tweak benchmark file

* chore: update test fixtures

* convert tt.privateName to PrivateIdentifier

* perf: avoid most isPrivateName call

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

Co-authored-by: Justin Ridgewell <justin@ridgewell.name>

* perf: use inlinable codePointAtPos

* make prettier happy

Co-authored-by: Justin Ridgewell <justin@ridgewell.name>
  • Loading branch information
JLHwung and jridgewell committed May 6, 2021
1 parent 278193b commit a387973
Show file tree
Hide file tree
Showing 24 changed files with 551 additions and 210 deletions.
2 changes: 2 additions & 0 deletions eslint/babel-eslint-parser/src/convert/convertTokens.js
Expand Up @@ -173,6 +173,8 @@ function convertToken(token, source) {
} else if (type === tt.bigint) {
token.type = "Numeric";
token.value = `${token.value}n`;
} else if (type === tt.privateName) {
token.type = "PrivateIdentifier";
}
if (typeof token.type !== "string") {
// Acorn does not have rightAssociative
Expand Down
35 changes: 24 additions & 11 deletions eslint/babel-eslint-parser/test/index.js
Expand Up @@ -320,17 +320,30 @@ describe("Babel and Espree", () => {
expect(babylonAST.tokens[1].type).toEqual("Punctuator");
});

// Espree doesn't support private fields yet
it("hash (token)", () => {
const code = "class A { #x }";
const babylonAST = parseForESLint(code, {
eslintVisitorKeys: true,
eslintScopeManager: true,
babelOptions: BABEL_OPTIONS,
}).ast;
expect(babylonAST.tokens[3].type).toEqual("Punctuator");
expect(babylonAST.tokens[3].value).toEqual("#");
});
if (process.env.BABEL_8_BREAKING) {
it("hash (token)", () => {
const code = "class A { #x }";
const babylonAST = parseForESLint(code, {
eslintVisitorKeys: true,
eslintScopeManager: true,
babelOptions: BABEL_OPTIONS,
}).ast;
expect(babylonAST.tokens[3].type).toEqual("PrivateIdentifier");
expect(babylonAST.tokens[3].value).toEqual("x");
});
} else {
// Espree doesn't support private fields yet
it("hash (token)", () => {
const code = "class A { #x }";
const babylonAST = parseForESLint(code, {
eslintVisitorKeys: true,
eslintScopeManager: true,
babelOptions: BABEL_OPTIONS,
}).ast;
expect(babylonAST.tokens[3].type).toEqual("Punctuator");
expect(babylonAST.tokens[3].value).toEqual("#");
});
}

it("parse to PropertyDeclaration when `classFeatures: true`", () => {
const code = "class A { #x }";
Expand Down
@@ -0,0 +1,34 @@
import Benchmark from "benchmark";
import baseline from "@babel-baseline/parser";
import current from "../../lib/index.js";
import { report } from "../util.mjs";

const suite = new Benchmark.Suite();
// All codepoints in [0x4e00, 0x9ffc] are valid identifier name per Unicode 13
function createInput(length) {
if (length > 0x9ffc - 0x4e00) {
throw new Error(
`Length greater than ${
0x9ffc - 0x4e00
} is not supported! Consider modify the \`createInput\`.`
);
}
let source = "class C { ";
for (let i = 0; i < length; i++) {
source += "#" + String.fromCharCode(0x4e00 + i) + ";";
}
return source + " }";
}
function benchCases(name, implementation, options) {
for (const length of [256, 512, 1024, 2048]) {
const input = createInput(length);
suite.add(`${name} ${length} length-1 private properties`, () => {
implementation.parse(input, options);
});
}
}

benchCases("baseline", baseline);
benchCases("current", current);

suite.on("cycle", report).run();
@@ -0,0 +1,19 @@
import Benchmark from "benchmark";
import baseline from "@babel-baseline/parser";
import current from "../../lib/index.js";
import { report } from "../util.mjs";

const suite = new Benchmark.Suite();

function benchCases(name, implementation, options) {
for (const length of [256, 512, 1024, 2048]) {
suite.add(`${name} ${length} empty statement`, () => {
implementation.parse(";".repeat(length), options);
});
}
}

benchCases("baseline", baseline);
benchCases("current + attachComment: false", current, { attachComment: false });

suite.on("cycle", report).run();
13 changes: 13 additions & 0 deletions packages/babel-parser/benchmark/util.mjs
@@ -0,0 +1,13 @@
export function report(event) {
const bench = event.target;
const factor = bench.hz < 100 ? 100 : 1;
const timeMs = bench.stats.mean * 1000;
const time =
timeMs < 10
? `${Math.round(timeMs * 1000) / 1000}ms`
: `${Math.round(timeMs)}ms`;
const msg = `${bench.name}: ${
Math.round(bench.hz * factor) / factor
} ops/sec 卤${Math.round(bench.stats.rme * 100) / 100}% (${time})`;
console.log(msg);
}
2 changes: 2 additions & 0 deletions packages/babel-parser/package.json
Expand Up @@ -33,9 +33,11 @@
"node": ">=6.0.0"
},
"devDependencies": {
"@babel-baseline/parser": "npm:@babel/parser@^7.14.0",
"@babel/code-frame": "workspace:*",
"@babel/helper-fixtures": "workspace:*",
"@babel/helper-validator-identifier": "workspace:*",
"benchmark": "^2.1.4",
"charcodes": "^0.2.0"
},
"bin": "./bin/babel-parser.js"
Expand Down
97 changes: 52 additions & 45 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -29,7 +29,8 @@ import {
isStrictBindReservedWord,
isIdentifierStart,
} from "../util/identifier";
import type { Pos, Position } from "../util/location";
import type { Pos } from "../util/location";
import { Position } from "../util/location";
import * as charCodes from "charcodes";
import {
BIND_OUTSIDE,
Expand Down Expand Up @@ -705,18 +706,19 @@ export default class ExpressionParser extends LValParser {
const computed = this.eat(tt.bracketL);
node.object = base;
node.computed = computed;
const privateName =
!computed && this.match(tt.privateName) && this.state.value;
const property = computed
? this.parseExpression()
: this.parseMaybePrivateName(true);
: privateName
? this.parsePrivateName()
: this.parseIdentifier(true);

if (this.isPrivateName(property)) {
if (privateName !== false) {
if (node.object.type === "Super") {
this.raise(startPos, Errors.SuperPrivateField);
}
this.classScope.usePrivateName(
this.getPrivateNameSV(property),
property.start,
);
this.classScope.usePrivateName(privateName, property.start);
}
node.property = property;

Expand Down Expand Up @@ -1160,6 +1162,23 @@ export default class ExpressionParser extends LValParser {
}
}

case tt.privateName: {
// https://tc39.es/proposal-private-fields-in-in
// RelationalExpression [In, Yield, Await]
// [+In] PrivateIdentifier in ShiftExpression[?Yield, ?Await]
const start = this.state.start;
const value = this.state.value;
node = this.parsePrivateName();
if (this.match(tt._in)) {
this.expectPlugin("privateIn");
this.classScope.usePrivateName(value, node.start);
} else if (this.hasPlugin("privateIn")) {
this.raise(this.state.start, Errors.PrivateInExpectedIn, value);
} else {
throw this.unexpected(start);
}
return node;
}
case tt.hash: {
if (this.state.inPipeline) {
node = this.startNode();
Expand All @@ -1179,32 +1198,6 @@ export default class ExpressionParser extends LValParser {
this.registerTopicReference();
return this.finishNode(node, "PipelinePrimaryTopicReference");
}

// https://tc39.es/proposal-private-fields-in-in
// RelationalExpression [In, Yield, Await]
// [+In] PrivateIdentifier in ShiftExpression[?Yield, ?Await]
const nextCh = this.input.codePointAt(this.state.end);
if (isIdentifierStart(nextCh) || nextCh === charCodes.backslash) {
const start = this.state.start;
// $FlowIgnore It'll either parse a PrivateName or throw.
node = (this.parseMaybePrivateName(true): N.PrivateName);
if (this.match(tt._in)) {
this.expectPlugin("privateIn");
this.classScope.usePrivateName(
this.getPrivateNameSV(node),
node.start,
);
} else if (this.hasPlugin("privateIn")) {
this.raise(
this.state.start,
Errors.PrivateInExpectedIn,
this.getPrivateNameSV(node),
);
} else {
throw this.unexpected(start);
}
return node;
}
}
// fall through
case tt.relational: {
Expand Down Expand Up @@ -1305,22 +1298,35 @@ export default class ExpressionParser extends LValParser {
parseMaybePrivateName(
isPrivateNameAllowed: boolean,
): N.PrivateName | N.Identifier {
const isPrivate = this.match(tt.hash);
const isPrivate = this.match(tt.privateName);

if (isPrivate) {
if (!isPrivateNameAllowed) {
this.raise(this.state.pos, Errors.UnexpectedPrivateField);
this.raise(this.state.start + 1, Errors.UnexpectedPrivateField);
}
const node = this.startNode();
this.next();
this.assertNoSpace("Unexpected space between # and identifier");
node.id = this.parseIdentifier(true);
return this.finishNode(node, "PrivateName");
return this.parsePrivateName();
} else {
return this.parseIdentifier(true);
}
}

parsePrivateName(): N.PrivateName {
const node = this.startNode();
const id = this.startNodeAt(
this.state.start + 1,
// The position is hardcoded because we merge `#` and name into a single
// tt.privateName token
new Position(
this.state.curLine,
this.state.start + 1 - this.state.lineStart,
),
);
const name = this.state.value;
this.next(); // eat #name;
node.id = this.createIdentifier(id, name);
return this.finishNode(node, "PrivateName");
}

parseFunctionOrFunctionSent(): N.FunctionExpression | N.MetaProperty {
const node = this.startNode();

Expand Down Expand Up @@ -1976,15 +1982,16 @@ export default class ExpressionParser extends LValParser {
const oldInPropertyName = this.state.inPropertyName;
this.state.inPropertyName = true;
// We check if it's valid for it to be a private name when we push it.
const type = this.state.type;
(prop: $FlowFixMe).key =
this.match(tt.num) ||
this.match(tt.string) ||
this.match(tt.bigint) ||
this.match(tt.decimal)
type === tt.num ||
type === tt.string ||
type === tt.bigint ||
type === tt.decimal
? this.parseExprAtom()
: this.parseMaybePrivateName(isPrivateNameAllowed);

if (!this.isPrivateName(prop.key)) {
if (type !== tt.privateName) {
// ClassPrivateProperty is never computed, so we don't assign in that case.
prop.computed = false;
}
Expand Down

0 comments on commit a387973

Please sign in to comment.