Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor private name tokenizing #13256

Merged
merged 11 commits into from May 6, 2021
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";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We convert tt.privateName to PrivateIdentifier, which aligns to eslint/espree#486

Note that although this PR merges tt.hash and tt.name to tt.privateName, this behaviour will be not observed by @babel/eslint-parsers because of the compat layer. However if we run with BABEL_8_BREAKING=true, the eslint parser will see tt.privateName, instead of breaking tt.privateName, we align it to the new espree behaviour.

}
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
96 changes: 51 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,18 @@ 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 +1161,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 +1197,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 +1297,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 +1981,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