[parser] Disallow duplicate and undeclared private names (#10456)
* [parser] Add private names tracking to Scope - Disallow duplicate private names - Disallow undeclared private names * Update tests * Test all possible duplications * Test undeclared private names * Better error message for top-level private names * Fix flow * Update test262 whitelist * Update fixtures * Update flow whitelist * Remove old output.json * Move ClassScopeHandler to a separate class * Make the code readable
This commit is contained in:
@@ -4,12 +4,14 @@ import type { Options } from "../options";
|
||||
import type State from "../tokenizer/state";
|
||||
import type { PluginsMap } from "./index";
|
||||
import type ScopeHandler from "../util/scope";
|
||||
import type ClassScopeHandler from "../util/class-scope";
|
||||
|
||||
export default class BaseParser {
|
||||
// Properties set by constructor in index.js
|
||||
options: Options;
|
||||
inModule: boolean;
|
||||
scope: ScopeHandler<*>;
|
||||
classScope: ClassScopeHandler;
|
||||
plugins: PluginsMap;
|
||||
filename: ?string;
|
||||
sawUnambiguousESM: boolean = false;
|
||||
|
||||
@@ -612,15 +612,21 @@ export default class ExpressionParser extends LValParser {
|
||||
? this.parseIdentifier(true)
|
||||
: this.parseMaybePrivateName();
|
||||
node.computed = computed;
|
||||
if (
|
||||
node.property.type === "PrivateName" &&
|
||||
node.object.type === "Super"
|
||||
) {
|
||||
this.raise(startPos, "Private fields can't be accessed on super");
|
||||
|
||||
if (node.property.type === "PrivateName") {
|
||||
if (node.object.type === "Super") {
|
||||
this.raise(startPos, "Private fields can't be accessed on super");
|
||||
}
|
||||
this.classScope.usePrivateName(
|
||||
node.property.id.name,
|
||||
node.property.start,
|
||||
);
|
||||
}
|
||||
|
||||
if (computed) {
|
||||
this.expect(tt.bracketR);
|
||||
}
|
||||
|
||||
if (state.optionalChainMember) {
|
||||
node.optional = optional;
|
||||
return this.finishNode(node, "OptionalMemberExpression");
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getOptions } from "../options";
|
||||
import StatementParser from "./statement";
|
||||
import { SCOPE_ASYNC, SCOPE_PROGRAM } from "../util/scopeflags";
|
||||
import ScopeHandler from "../util/scope";
|
||||
import ClassScopeHandler from "../util/class-scope";
|
||||
|
||||
export type PluginsMap = Map<string, { [string]: any }>;
|
||||
|
||||
@@ -25,6 +26,7 @@ export default class Parser extends StatementParser {
|
||||
this.options = options;
|
||||
this.inModule = this.options.sourceType === "module";
|
||||
this.scope = new ScopeHandler(this.raise.bind(this), this.inModule);
|
||||
this.classScope = new ClassScopeHandler(this.raise.bind(this));
|
||||
this.plugins = pluginsMap(this.options.plugins);
|
||||
this.filename = options.sourceFilename;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ import {
|
||||
SCOPE_OTHER,
|
||||
SCOPE_SIMPLE_CATCH,
|
||||
SCOPE_SUPER,
|
||||
CLASS_ELEMENT_OTHER,
|
||||
CLASS_ELEMENT_INSTANCE_GETTER,
|
||||
CLASS_ELEMENT_INSTANCE_SETTER,
|
||||
CLASS_ELEMENT_STATIC_GETTER,
|
||||
CLASS_ELEMENT_STATIC_SETTER,
|
||||
type BindingTypes,
|
||||
} from "../util/scopeflags";
|
||||
|
||||
@@ -1171,7 +1176,7 @@ export default class StatementParser extends ExpressionParser {
|
||||
}
|
||||
|
||||
parseClassBody(constructorAllowsSuper: boolean): N.ClassBody {
|
||||
this.state.classLevel++;
|
||||
this.classScope.enter();
|
||||
|
||||
const state = { hadConstructor: false };
|
||||
let decorators: N.Decorator[] = [];
|
||||
@@ -1231,7 +1236,7 @@ export default class StatementParser extends ExpressionParser {
|
||||
);
|
||||
}
|
||||
|
||||
this.state.classLevel--;
|
||||
this.classScope.exit();
|
||||
|
||||
return this.finishNode(classBody, "ClassBody");
|
||||
}
|
||||
@@ -1514,7 +1519,15 @@ export default class StatementParser extends ExpressionParser {
|
||||
prop: N.ClassPrivateProperty,
|
||||
) {
|
||||
this.expectPlugin("classPrivateProperties", prop.key.start);
|
||||
classBody.body.push(this.parseClassPrivateProperty(prop));
|
||||
|
||||
const node = this.parseClassPrivateProperty(prop);
|
||||
classBody.body.push(node);
|
||||
|
||||
this.classScope.declarePrivateName(
|
||||
node.key.id.name,
|
||||
CLASS_ELEMENT_OTHER,
|
||||
node.key.start,
|
||||
);
|
||||
}
|
||||
|
||||
pushClassMethod(
|
||||
@@ -1545,17 +1558,29 @@ export default class StatementParser extends ExpressionParser {
|
||||
isAsync: boolean,
|
||||
): void {
|
||||
this.expectPlugin("classPrivateMethods", method.key.start);
|
||||
classBody.body.push(
|
||||
this.parseMethod(
|
||||
method,
|
||||
isGenerator,
|
||||
isAsync,
|
||||
/* isConstructor */ false,
|
||||
false,
|
||||
"ClassPrivateMethod",
|
||||
true,
|
||||
),
|
||||
|
||||
const node = this.parseMethod(
|
||||
method,
|
||||
isGenerator,
|
||||
isAsync,
|
||||
/* isConstructor */ false,
|
||||
false,
|
||||
"ClassPrivateMethod",
|
||||
true,
|
||||
);
|
||||
classBody.body.push(node);
|
||||
|
||||
const kind =
|
||||
node.kind === "get"
|
||||
? node.static
|
||||
? CLASS_ELEMENT_STATIC_GETTER
|
||||
: CLASS_ELEMENT_INSTANCE_GETTER
|
||||
: node.kind === "set"
|
||||
? node.static
|
||||
? CLASS_ELEMENT_STATIC_SETTER
|
||||
: CLASS_ELEMENT_INSTANCE_SETTER
|
||||
: CLASS_ELEMENT_OTHER;
|
||||
this.classScope.declarePrivateName(node.key.id.name, kind, node.key.start);
|
||||
}
|
||||
|
||||
// Overridden in typescript.js
|
||||
|
||||
@@ -391,14 +391,8 @@ export default class Tokenizer extends LocationParser {
|
||||
}
|
||||
|
||||
if (
|
||||
(this.hasPlugin("classPrivateProperties") ||
|
||||
this.hasPlugin("classPrivateMethods")) &&
|
||||
this.state.classLevel > 0
|
||||
) {
|
||||
++this.state.pos;
|
||||
this.finishToken(tt.hash);
|
||||
return;
|
||||
} else if (
|
||||
this.hasPlugin("classPrivateProperties") ||
|
||||
this.hasPlugin("classPrivateMethods") ||
|
||||
this.getPluginOption("pipelineOperator", "proposal") === "smart"
|
||||
) {
|
||||
this.finishOp(tt.hash, 1);
|
||||
|
||||
@@ -77,9 +77,6 @@ export default class State {
|
||||
soloAwait: boolean = false;
|
||||
inFSharpPipelineDirectBody: boolean = false;
|
||||
|
||||
// Check whether we are in a (nested) class or not.
|
||||
classLevel: number = 0;
|
||||
|
||||
// Labels in scope.
|
||||
labels: Array<{
|
||||
kind: ?("loop" | "switch"),
|
||||
|
||||
113
packages/babel-parser/src/util/class-scope.js
Normal file
113
packages/babel-parser/src/util/class-scope.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
CLASS_ELEMENT_KIND_ACCESSOR,
|
||||
CLASS_ELEMENT_FLAG_STATIC,
|
||||
type ClassElementTypes,
|
||||
} from "./scopeflags";
|
||||
|
||||
export class ClassScope {
|
||||
// A list of private named declared in the current class
|
||||
privateNames: Set<string> = new Set();
|
||||
|
||||
// A list of private getters of setters without their counterpart
|
||||
loneAccessors: Map<string, ClassElementTypes> = new Map();
|
||||
|
||||
// A list of private names used before being defined, mapping to
|
||||
// their position.
|
||||
undefinedPrivateNames: Map<string, number> = new Map();
|
||||
}
|
||||
|
||||
type raiseFunction = (number, string) => void;
|
||||
|
||||
export default class ClassScopeHandler {
|
||||
stack: Array<ClassScope> = [];
|
||||
raise: raiseFunction;
|
||||
undefinedPrivateNames: Map<string, number> = new Map();
|
||||
|
||||
constructor(raise: raiseFunction) {
|
||||
this.raise = raise;
|
||||
}
|
||||
|
||||
current(): ClassScope {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
enter() {
|
||||
this.stack.push(new ClassScope());
|
||||
}
|
||||
|
||||
exit() {
|
||||
const oldClassScope = this.stack.pop();
|
||||
|
||||
// Migrate the usage of not yet defined private names to the outer
|
||||
// class scope, or raise an error if we reached the top-level scope.
|
||||
|
||||
const current = this.current();
|
||||
|
||||
// Array.from is needed because this is compiled to an array-like for loop
|
||||
for (const [name, pos] of Array.from(oldClassScope.undefinedPrivateNames)) {
|
||||
if (current) {
|
||||
if (!current.undefinedPrivateNames.has(name)) {
|
||||
current.undefinedPrivateNames.set(name, pos);
|
||||
}
|
||||
} else {
|
||||
this.raiseUndeclaredPrivateName(name, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declarePrivateName(
|
||||
name: string,
|
||||
elementType: ClassElementTypes,
|
||||
pos: number,
|
||||
) {
|
||||
const classScope = this.current();
|
||||
let redefined = classScope.privateNames.has(name);
|
||||
|
||||
if (elementType & CLASS_ELEMENT_KIND_ACCESSOR) {
|
||||
const accessor = redefined && classScope.loneAccessors.get(name);
|
||||
if (accessor) {
|
||||
const oldStatic = accessor & CLASS_ELEMENT_FLAG_STATIC;
|
||||
const newStatic = elementType & CLASS_ELEMENT_FLAG_STATIC;
|
||||
|
||||
const oldKind = accessor & CLASS_ELEMENT_KIND_ACCESSOR;
|
||||
const newKind = elementType & CLASS_ELEMENT_KIND_ACCESSOR;
|
||||
|
||||
// The private name can be duplicated only if it is used by
|
||||
// two accessors with different kind (get and set), and if
|
||||
// they have the same placement (static or not).
|
||||
redefined = oldKind === newKind || oldStatic !== newStatic;
|
||||
|
||||
if (!redefined) classScope.loneAccessors.delete(name);
|
||||
} else if (!redefined) {
|
||||
classScope.loneAccessors.set(name, elementType);
|
||||
}
|
||||
}
|
||||
|
||||
if (redefined) {
|
||||
this.raise(pos, `Duplicate private name #${name}`);
|
||||
}
|
||||
|
||||
classScope.privateNames.add(name);
|
||||
classScope.undefinedPrivateNames.delete(name);
|
||||
}
|
||||
|
||||
usePrivateName(name: string, pos: number) {
|
||||
let classScope;
|
||||
for (classScope of this.stack) {
|
||||
if (classScope.privateNames.has(name)) return;
|
||||
}
|
||||
|
||||
if (classScope) {
|
||||
classScope.undefinedPrivateNames.set(name, pos);
|
||||
} else {
|
||||
// top-level
|
||||
this.raiseUndeclaredPrivateName(name, pos);
|
||||
}
|
||||
}
|
||||
|
||||
raiseUndeclaredPrivateName(name: string, pos: number) {
|
||||
this.raise(pos, `Private name #${name} is not defined`);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export default class ScopeHandler<IScope: Scope = Scope> {
|
||||
raise: raiseFunction;
|
||||
inModule: boolean;
|
||||
undefinedExports: Map<string, number> = new Map();
|
||||
undefinedPrivateNames: Map<string, number> = new Map();
|
||||
|
||||
constructor(raise: raiseFunction, inModule: boolean) {
|
||||
this.raise = raise;
|
||||
|
||||
@@ -84,3 +84,23 @@ export type BindingTypes =
|
||||
| typeof BIND_TS_ENUM
|
||||
| typeof BIND_TS_AMBIENT
|
||||
| typeof BIND_TS_NAMESPACE;
|
||||
|
||||
// prettier-ignore
|
||||
export const CLASS_ELEMENT_FLAG_STATIC = 0b1_00,
|
||||
CLASS_ELEMENT_KIND_GETTER = 0b0_10,
|
||||
CLASS_ELEMENT_KIND_SETTER = 0b0_01,
|
||||
CLASS_ELEMENT_KIND_ACCESSOR = CLASS_ELEMENT_KIND_GETTER | CLASS_ELEMENT_KIND_SETTER;
|
||||
|
||||
// prettier-ignore
|
||||
export const CLASS_ELEMENT_STATIC_GETTER = CLASS_ELEMENT_KIND_GETTER | CLASS_ELEMENT_FLAG_STATIC,
|
||||
CLASS_ELEMENT_STATIC_SETTER = CLASS_ELEMENT_KIND_SETTER | CLASS_ELEMENT_FLAG_STATIC,
|
||||
CLASS_ELEMENT_INSTANCE_GETTER = CLASS_ELEMENT_KIND_GETTER,
|
||||
CLASS_ELEMENT_INSTANCE_SETTER = CLASS_ELEMENT_KIND_SETTER,
|
||||
CLASS_ELEMENT_OTHER = 0;
|
||||
|
||||
export type ClassElementTypes =
|
||||
| typeof CLASS_ELEMENT_STATIC_GETTER
|
||||
| typeof CLASS_ELEMENT_STATIC_SETTER
|
||||
| typeof CLASS_ELEMENT_INSTANCE_GETTER
|
||||
| typeof CLASS_ELEMENT_INSTANCE_SETTER
|
||||
| typeof CLASS_ELEMENT_OTHER;
|
||||
|
||||
Reference in New Issue
Block a user