Scope Analysis
After parsing the body has finished, the scope analysis phase has also been completed.
src/util/scope.js: ClassScope and ClassScopeHandler
The code for the scope analysis associated with the parser seems to be in the folder /packages/babel-parser/src/util
babel-parser
βββ src
βββ util
βββ class-scope.js
βββ identifier.js
βββ location.js
βββ production-parameter.js
βββ scope.js
βββ scopeflags.js
βββ whitespace.js
and mainly in the files scopeflags.js, class-scope.js and scope.js.
Let us have a look at classes Scope Class and the class ClassScopeHandler inside src/util/scope.js
ClassScope
privateNames
is aSet
of private named declared in the current classundefinedPrivateNames
is aMap
of private names used before being defined, mapping to their position.
export class ClassScope {
privateNames: Set<string> = new Set();
loneAccessors: Map<string, ClassElementTypes> = new Map(); // for getters and setters
undefinedPrivateNames: Map<string, number> = new Map();
}
The loneAccessors
attribute is for getters and setters.
Read Property getters and setters for a gentle introduction to getters and setters in JS.
ClassScopeHandler
The class ClassScopeHandler:
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.raise(pos, Errors.InvalidPrivateFieldResolution, name);
}
}
}
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, Errors.PrivateNameRedeclaration, 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.raise(pos, Errors.InvalidPrivateFieldResolution, name);
}
}
}
src/parser/base.js: BaseParser
scope.js
and class-scope.js
types are imported, reformatted and exported again by the babel-parser/src/parser/base.js module:
...
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; // Configurations options
inModule: boolean; // True if the code is in a module
scope: ScopeHandler<*>; // Generic type
classScope: ClassScopeHandler;
...
state: State; // Initialized by Tokenizer
// input and length are not in state as they are constant and we do
// not want to ever copy them, which happens if state gets cloned
input: string;
length: number;
hasPlugin(name: string): boolean { return this.plugins.has(name); } // checks if a given plugin is available in the plugins map
getPluginOption(plugin: string, name: string) { if (this.hasPlugin(plugin)) return this.plugins.get(plugin)[name]; } // Retrieves an option for a specified plugin
}
src/parser/comments.js: CommentsParser Class
The BaseParser
class is imported by the comments.js
module which adds the CommentsParser
class to it:
import BaseParser from "./base";
...
export default class CommentsParser extends BaseParser { ... }
error.js
The CommentsParser
class is imported by the error.js
module.
src
βββ parser
β βββ base.js
β βββ comments.js
β βββ error-message.js
β βββ error.js
β βββ expression.js
β βββ index.js
β βββ lval.js
β βββ node.js
β βββ statement.js
β βββ util.js
The
ParserError
class inherits from the CommentsParser
:
import { getLineInfo, type Position } from "../util/location";
import CommentsParser from "./comments";
type ErrorContext = {
pos: number,
loc: Position,
missingPlugin?: Array<string>,
code?: string,
};
export { ErrorMessages as Errors } from "./error-message.js";
export default class ParserError extends CommentsParser { ... }
β>
src/parser/index.js: Parser Class
The ScopeHandler
and ClassScopeHandler
classes are imported by the Parser
class in the babel-parser/src/parser/index.js module:
import type { Options } from "../options";
import type { File /*::, JSXOpeningElement */ } from "../types";
import type { PluginList } from "../plugin-utils";
import { getOptions } from "../options";
import StatementParser from "./statement";
import { SCOPE_PROGRAM } from "../util/scopeflags";
import ScopeHandler from "../util/scope";
import ClassScopeHandler from "../util/class-scope";
import ProductionParameterHandler, { PARAM_AWAIT, PARAM, } from "../util/production-parameter";
export type PluginsMap = Map<string, { [string]: any }>;
export default class Parser extends StatementParser { ... }
Here is the class Parser
in full:
export default class Parser extends StatementParser {
constructor(options: ?Options, input: string) { ... }
getScopeHandler(): Class<ScopeHandler<*>> { return ScopeHandler; }
parse(): File { ... }
}
function pluginsMap(plugins: PluginList): PluginsMap {
const pluginMap: PluginsMap = new Map();
for (const plugin of plugins) {
const [name, options] = Array.isArray(plugin) ? plugin : [plugin, {}];
if (!pluginMap.has(name)) pluginMap.set(name, options || {});
}
return pluginMap;
}
Constructor
constructor(options: ?Options, input: string) {
options = getOptions(options);
super(options, input);
const ScopeHandler = this.getScopeHandler();
this.options = options;
this.inModule = this.options.sourceType === "module";
this.scope = new ScopeHandler(this.raise.bind(this), this.inModule);
this.prodParam = new ProductionParameterHandler();
this.classScope = new ClassScopeHandler(this.raise.bind(this));
this.plugins = pluginsMap(this.options.plugins);
this.filename = options.sourceFilename;
}
parse
method
The call this.scope.enter(SCOPE_PROGRAM)
enters the program scope.
The call to this.parseTopLevel(file, program);
starts the parsing at the top level.
parse(): File {
let paramFlags = PARAM;
if (this.hasPlugin("topLevelAwait") && this.inModule) {
paramFlags |= PARAM_AWAIT;
}
this.scope.enter(SCOPE_PROGRAM);
this.prodParam.enter(paramFlags);
const file = this.startNode();
const program = this.startNode();
this.nextToken();
file.errors = null;
this.parseTopLevel(file, program);
file.errors = this.state.errors;
return file;
}
}