[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:
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