| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Haijie Xie @hai-x
- */
- "use strict";
- /**
- * @import {
- * Function as ESTreeFunction,
- * Identifier,
- * Node,
- * Program
- * } from "estree"
- */
- /**
- * @typedef {"global" | "module" | "function" | "function-expression-name" | "block" | "switch" | "catch" | "with" | "for" | "class" | "class-field-initializer" | "class-static-block"} ScopeType
- */
- /**
- * Acorn records every node's offset as `start`, but the nodes are typed as
- * `estree`, which models the spec and has only the optional `range`/`loc`.
- * Intersect with this to read the offset the parser really wrote.
- * @typedef {{ start: number }} Offset
- */
- /**
- * A node's visitable slots, as `CHILD_KEYS` names them.
- * @typedef {Record<string, Node | Node[] | null | undefined>} NodeChildren
- */
- /**
- * Child properties to visit for node types with no special scoping or
- * referencing behaviour. Types handled by the walker's `switch` never reach
- * this table.
- * @type {Record<string, string[]>}
- */
- const CHILD_KEYS = {
- ArrayExpression: ["elements"],
- ArrayPattern: ["elements"],
- ArrowFunctionExpression: ["params", "body"],
- AssignmentExpression: ["left", "right"],
- AssignmentPattern: ["left", "right"],
- AwaitExpression: ["argument"],
- BinaryExpression: ["left", "right"],
- BlockStatement: ["body"],
- BreakStatement: ["label"],
- CallExpression: ["callee", "arguments"],
- CatchClause: ["param", "body"],
- ChainExpression: ["expression"],
- ClassBody: ["body"],
- ClassDeclaration: ["id", "superClass", "body"],
- ClassExpression: ["id", "superClass", "body"],
- ConditionalExpression: ["test", "consequent", "alternate"],
- ContinueStatement: ["label"],
- DoWhileStatement: ["body", "test"],
- EmptyStatement: [],
- ExportAllDeclaration: ["source"],
- ExportDefaultDeclaration: ["declaration"],
- ExportNamedDeclaration: ["declaration", "specifiers", "source"],
- ExportSpecifier: ["exported", "local"],
- ExpressionStatement: ["expression"],
- ForInStatement: ["left", "right", "body"],
- ForOfStatement: ["left", "right", "body"],
- ForStatement: ["init", "test", "update", "body"],
- FunctionDeclaration: ["id", "params", "body"],
- FunctionExpression: ["id", "params", "body"],
- Identifier: [],
- IfStatement: ["test", "consequent", "alternate"],
- ImportDeclaration: ["specifiers", "source"],
- ImportDefaultSpecifier: ["local"],
- ImportExpression: ["source", "options"],
- ImportNamespaceSpecifier: ["local"],
- ImportSpecifier: ["imported", "local"],
- LabeledStatement: ["label", "body"],
- Literal: [],
- LogicalExpression: ["left", "right"],
- MemberExpression: ["object", "property"],
- MetaProperty: ["meta", "property"],
- MethodDefinition: ["key", "value"],
- NewExpression: ["callee", "arguments"],
- ObjectExpression: ["properties"],
- ObjectPattern: ["properties"],
- PrivateIdentifier: [],
- Program: ["body"],
- Property: ["key", "value"],
- PropertyDefinition: ["key", "value"],
- RestElement: ["argument"],
- ReturnStatement: ["argument"],
- SequenceExpression: ["expressions"],
- SpreadElement: ["argument"],
- StaticBlock: ["body"],
- Super: [],
- SwitchCase: ["test", "consequent"],
- SwitchStatement: ["discriminant", "cases"],
- TaggedTemplateExpression: ["tag", "quasi"],
- TemplateElement: [],
- TemplateLiteral: ["quasis", "expressions"],
- ThisExpression: [],
- ThrowStatement: ["argument"],
- TryStatement: ["block", "handler", "finalizer"],
- UnaryExpression: ["argument"],
- UpdateExpression: ["argument"],
- VariableDeclaration: ["declarations"],
- VariableDeclarator: ["id", "init"],
- WhileStatement: ["test", "body"],
- WithStatement: ["object", "body"],
- YieldExpression: ["argument"]
- };
- /**
- * A lexical scope. One shape for every kind, so the property loads in the
- * resolution loop stay monomorphic.
- */
- class Scope {
- /**
- * @param {ScopeType} type what opened this scope
- * @param {Node} block the node that opened this scope
- * @param {Scope | null} upper the enclosing scope
- * @param {boolean} isVarScope whether `var` and function declarations hoist to here
- * @param {boolean} recordEveryReference whether every binding collects its references, not only the ones webpack reads
- */
- constructor(type, block, upper, isVarScope, recordEveryReference) {
- /** @type {ScopeType} */
- this.type = type;
- /** @type {Node} */
- this.block = block;
- /** @type {Scope | null} */
- this.upper = upper;
- /** @type {Scope[]} */
- this.childScopes = NO_CHILD_SCOPES;
- /** @type {Variable[]} */
- this.variables = NO_VARIABLE_LIST;
- /** @type {Map<string, Variable> | undefined} the index, once this scope outgrows a scan */
- this._index = undefined;
- /** @type {Scope} the nearest enclosing scope `var` hoists to */
- this.variableScope = isVarScope
- ? this
- : /** @type {Scope} */ (upper).variableScope;
- /**
- * For a function scope with parameters, the offset where its body
- * starts; `-1` for every other scope. Separates the two regions that
- * share this scope, so a reference in the parameter list can be kept
- * from resolving to a binding declared in the body — see
- * `isHiddenBodyBinding`.
- * @type {number}
- */
- this.paramBoundary = -1;
- /**
- * Whether this scope's bindings collect their references — only the
- * module scope and its direct children are ever asked. See `_resolve`.
- * @type {boolean}
- */
- this._recorded =
- recordEveryReference ||
- type === "module" ||
- (upper !== null && upper.type === "module");
- if (upper !== null) {
- if (upper.childScopes === NO_CHILD_SCOPES) upper.childScopes = [this];
- else upper.childScopes.push(this);
- }
- }
- /**
- * @param {string} name name to look up
- * @returns {Variable | undefined} the binding this scope declares under that name
- */
- getBinding(name) {
- const index = this._index;
- if (index !== undefined) return index.get(name);
- const variables = this.variables;
- for (let i = 0; i < variables.length; i++) {
- const variable = variables[i];
- if (variable.name === name) return variable;
- }
- return undefined;
- }
- }
- /** A binding: one name declared in one scope. */
- class Variable {
- /**
- * @param {string} name the declared name
- * @param {Scope} scope the declaring scope
- */
- constructor(name, scope) {
- /** @type {string} */
- this.name = name;
- /** @type {Identifier[]} declaring occurrences */
- this.identifiers = NO_IDENTIFIERS;
- /** @type {Reference[]} occurrences that resolved here, at any depth */
- this.references = NO_REFERENCES;
- /** @type {Scope} */
- this.scope = scope;
- }
- }
- /** One identifier occurrence that refers to a binding. */
- class Reference {
- /**
- * @param {Identifier} identifier the identifier node
- * @param {Scope} from the scope the identifier was seen in
- */
- constructor(identifier, from) {
- /** @type {Identifier} */
- this.identifier = identifier;
- /** @type {Scope} */
- this.from = from;
- /** @type {Variable | undefined} set during resolution, absent when free */
- this.resolved = undefined;
- }
- }
- /**
- * Returned by `_pattern` for a plain identifier. Never mutated by callers.
- * @type {Node[]}
- */
- const NO_RIGHT_HAND_NODES = [];
- /**
- * Stand-ins for the collections of a scope that declares nothing and encloses
- * nothing — over half of them are one or both. Each is replaced by a real
- * collection when the scope first needs it. None of them is ever mutated.
- * @type {Variable[]}
- */
- const NO_VARIABLE_LIST = [];
- /** @type {Scope[]} */
- const NO_CHILD_SCOPES = [];
- /** @type {Reference[]} */
- const NO_REFERENCES = [];
- /** @type {Identifier[]} */
- const NO_IDENTIFIERS = [];
- /**
- * Above this many bindings a scope indexes them in a `Map`; below it, scanning
- * the list is as fast and costs no second structure. ~96% of the scopes that
- * bind anything stay below.
- */
- const INDEX_THRESHOLD = 8;
- /**
- * What `_pattern` does with each name it binds. An integer rather than a
- * callback, so walking a pattern allocates no closure.
- */
- const PATTERN_DEFINE = 0;
- const PATTERN_DEFINE_INIT = 1;
- const PATTERN_REFERENCE = 2;
- const PATTERN_REFERENCE_PLAIN = 3;
- /**
- * Adds a binding to a scope, seeding its list on the first one and building an
- * index once the scope has outgrown a scan.
- * @param {Scope} scope the scope that just declared a name
- * @param {Variable} variable the binding it declared
- * @returns {void}
- */
- const addBinding = (scope, variable) => {
- const variables = scope.variables;
- if (variables === NO_VARIABLE_LIST) {
- scope.variables = [variable];
- return;
- }
- variables.push(variable);
- const index = scope._index;
- if (index !== undefined) {
- index.set(variable.name, variable);
- return;
- }
- if (variables.length <= INDEX_THRESHOLD) return;
- /** @type {Map<string, Variable>} */
- const built = new Map();
- for (let i = 0; i < variables.length; i++) {
- built.set(variables[i].name, variables[i]);
- }
- scope._index = built;
- };
- /**
- * Statement types that provably declare nothing in the block that holds them —
- * `var` hoists past it, and the rest bind nowhere. Anything else, a syntax
- * this list has not heard of included, is assumed to declare.
- * @type {Set<string>}
- */
- const STATEMENTS_WITHOUT_BINDINGS = new Set([
- "BlockStatement",
- "BreakStatement",
- "ContinueStatement",
- "DebuggerStatement",
- "DoWhileStatement",
- "EmptyStatement",
- "ExpressionStatement",
- "ForInStatement",
- "ForOfStatement",
- "ForStatement",
- "IfStatement",
- "ReturnStatement",
- "SwitchStatement",
- "ThrowStatement",
- "TryStatement",
- "WhileStatement",
- "WithStatement"
- ]);
- /**
- * Whether a statement list needs a scope of its own to hold what it declares.
- * @param {(Node | null | undefined)[]} body statement list
- * @returns {boolean} true when a scope has to be opened for it
- */
- const needsScope = (body) => {
- for (let i = 0; i < body.length; i++) {
- const statement = body[i];
- if (statement === null || statement === undefined) continue;
- const type = statement.type;
- if (type === "VariableDeclaration") {
- if (statement.kind !== "var") return true;
- } else if (type === "SwitchCase") {
- if (needsScope(statement.consequent)) return true;
- } else if (!STATEMENTS_WITHOUT_BINDINGS.has(type)) {
- return true;
- }
- }
- return false;
- };
- /**
- * Node types that may appear as an assignment or binding target.
- * @param {Node} node node to test
- * @returns {boolean} true when the node can hold bindings
- */
- const isPattern = (node) => {
- const type = node.type;
- return (
- type === "Identifier" ||
- type === "ObjectPattern" ||
- type === "ArrayPattern" ||
- type === "SpreadElement" ||
- type === "RestElement" ||
- type === "AssignmentPattern"
- );
- };
- /**
- * Whether a binding found in a function scope is invisible to a reference,
- * because the reference sits in the parameter list and the binding is declared
- * in the body.
- *
- * Parameters and body share one scope here, but the language gives them two:
- * a function with parameters evaluates them first, and only then creates the
- * environment its body declarations live in. So a parameter default reads the
- * enclosing scope, never the body.
- *
- * A name declared in *both* places is not hidden — that is what keeps the `x`
- * in `function f(x) { var x = 1; return x }` resolving to the parameter.
- * @param {Variable} variable a binding found in a function scope with parameters
- * @param {Identifier} identifier the identifier being resolved
- * @param {number} boundary source offset where that function's body starts
- * @returns {boolean} true when resolution must skip this binding and climb
- * @example
- * ```js
- * const x = 1;
- * function f(a = x) { const x = 2; }
- * // the default reads the outer `x`; the body's `x` does not exist yet
- * ```
- */
- const isHiddenBodyBinding = (variable, identifier, boundary) => {
- // a reference in the body sees everything the scope holds
- if (/** @type {Identifier & Offset} */ (identifier).start >= boundary) {
- return false;
- }
- const identifiers = variable.identifiers;
- for (let i = 0; i < identifiers.length; i++) {
- // declared in the parameter list too, so the parameters do see it
- if (/** @type {Identifier & Offset} */ (identifiers[i]).start < boundary) {
- return false;
- }
- }
- return true;
- };
- class ScopeAnalyzer {
- /**
- * @param {boolean} recordEveryReference whether every binding collects its references
- */
- constructor(recordEveryReference) {
- /** @type {Scope} the scope the walker is currently inside */
- this.scope = /** @type {Scope} */ (/** @type {unknown} */ (null));
- /** @type {Identifier[]} identifiers awaiting resolution */
- this.pendingIdentifiers = [];
- /** @type {Scope[]} the scope each pending identifier was seen in */
- this.pendingScopes = [];
- /** @type {boolean} */
- this.recordEveryReference = recordEveryReference;
- }
- /**
- * @param {ScopeType} type scope kind
- * @param {Node} block node opening the scope
- * @param {boolean} isVarScope whether `var` hoists to here
- * @returns {Scope} the new scope, now current
- */
- _push(type, block, isVarScope) {
- const scope = new Scope(
- type,
- block,
- this.scope,
- isVarScope,
- this.recordEveryReference
- );
- this.scope = scope;
- return scope;
- }
- /**
- * @returns {void}
- */
- _pop() {
- this.scope = /** @type {Scope} */ (this.scope.upper);
- }
- /**
- * Declares a name in a scope, reusing the binding when it already exists
- * (`var x; var x;`, a function and its hoisted declaration, and so on).
- * @param {Scope} scope scope to declare in
- * @param {Node | null} node the declaring identifier, absent for an anonymous `export default` declaration
- * @returns {void}
- */
- _define(scope, node) {
- if (node === null || node.type !== "Identifier") return;
- const name = node.name;
- let variable = scope.getBinding(name);
- if (variable === undefined) {
- variable = new Variable(name, scope);
- addBinding(scope, variable);
- }
- if (variable.identifiers === NO_IDENTIFIERS) {
- variable.identifiers = [/** @type {Identifier} */ (node)];
- } else {
- variable.identifiers.push(/** @type {Identifier} */ (node));
- }
- }
- /**
- * Records an identifier occurrence to be resolved once the walk is done.
- * @param {Node} node the identifier
- * @returns {void}
- */
- _reference(node) {
- this.pendingIdentifiers.push(/** @type {Identifier} */ (node));
- this.pendingScopes.push(this.scope);
- }
- /**
- * Binds or references one name of a pattern, as `mode` asks.
- * @param {Node} node the bound identifier
- * @param {number} defaults number of enclosing defaults
- * @param {number} mode one of the `PATTERN_*` constants
- * @param {Scope} target scope to declare in
- * @returns {void}
- */
- _bind(node, defaults, mode, target) {
- if (mode <= PATTERN_DEFINE_INIT) this._define(target, node);
- if (mode !== PATTERN_REFERENCE_PLAIN) {
- for (let d = 0; d < defaults; d++) this._reference(node);
- }
- if (mode >= PATTERN_DEFINE_INIT) this._reference(node);
- }
- /**
- * Walks a binding pattern, binding each name it holds. The expressions
- * inside it are returned rather than visited, so every name binds first.
- * @param {Node} root the pattern
- * @param {number} mode one of the `PATTERN_*` constants
- * @param {Scope} target scope to declare in
- * @returns {Node[]} expressions still to visit
- */
- _pattern(root, mode, target) {
- // a bare identifier is ~97% of calls and holds no expressions, so it
- // needs neither the result array nor a walk
- if (root.type === "Identifier") {
- this._bind(root, 0, mode, target);
- return NO_RIGHT_HAND_NODES;
- }
- /** @type {Node[]} */
- const rightHandNodes = [];
- this._patternWalk(root, mode, target, 0, rightHandNodes);
- return rightHandNodes;
- }
- /**
- * @param {Node | null} node current pattern node
- * @param {number} mode one of the `PATTERN_*` constants
- * @param {Scope} target scope to declare in
- * @param {number} defaults number of enclosing defaults
- * @param {Node[]} rightHandNodes collects the expressions still to visit
- * @returns {void}
- */
- _patternWalk(node, mode, target, defaults, rightHandNodes) {
- if (node === null || node === undefined) return;
- switch (node.type) {
- case "Identifier":
- this._bind(node, defaults, mode, target);
- return;
- case "ObjectPattern":
- for (const property of node.properties) {
- this._patternWalk(property, mode, target, defaults, rightHandNodes);
- }
- return;
- case "ArrayPattern":
- for (const element of node.elements) {
- this._patternWalk(element, mode, target, defaults, rightHandNodes);
- }
- return;
- case "Property":
- if (node.computed) rightHandNodes.push(node.key);
- this._patternWalk(node.value, mode, target, defaults, rightHandNodes);
- return;
- case "AssignmentPattern":
- this._patternWalk(
- node.left,
- mode,
- target,
- defaults + 1,
- rightHandNodes
- );
- rightHandNodes.push(node.right);
- return;
- case "RestElement":
- case "SpreadElement":
- this._patternWalk(
- node.argument,
- mode,
- target,
- defaults,
- rightHandNodes
- );
- return;
- case "MemberExpression":
- // the object is only read; the write lands on its property
- if (node.computed) rightHandNodes.push(node.property);
- rightHandNodes.push(node.object);
- return;
- // assignment targets the parser reports as expressions
- case "ArrayExpression":
- for (const element of node.elements) {
- this._patternWalk(element, mode, target, defaults, rightHandNodes);
- }
- return;
- case "ObjectExpression":
- for (const property of node.properties) {
- this._patternWalk(property, mode, target, defaults, rightHandNodes);
- }
- return;
- case "AssignmentExpression":
- this._patternWalk(
- node.left,
- mode,
- target,
- defaults + 1,
- rightHandNodes
- );
- rightHandNodes.push(node.right);
- return;
- default:
- rightHandNodes.push(node);
- }
- }
- /**
- * @param {(Node | null | undefined)[]} nodes statement or expression list, holes and all
- * @returns {void}
- */
- _visitAll(nodes) {
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i];
- if (node !== null && node !== undefined) this._visit(node);
- }
- }
- /**
- * A function's parameters and body share one scope, so a named function
- * expression gets an extra scope above it holding only its own name.
- * @param {ESTreeFunction} node Function node
- * @returns {void}
- */
- _visitFunction(node) {
- if (node.type === "FunctionDeclaration") {
- // block scoped in ES6, so it lands in the enclosing scope
- this._define(this.scope, /** @type {Node} */ (node.id));
- }
- const named =
- node.type === "FunctionExpression" &&
- node.id !== null &&
- node.id !== undefined;
- if (named) {
- this._push("function-expression-name", node, false);
- this._define(this.scope, /** @type {Node} */ (node.id));
- }
- const scope = this._push("function", node, true);
- if (node.type !== "ArrowFunctionExpression") {
- // every non-arrow function has an implicit `arguments`
- addBinding(scope, new Variable("arguments", scope));
- }
- const params = node.params;
- if (params.length !== 0) {
- scope.paramBoundary = /** @type {Node & Offset} */ (node.body).start;
- for (let i = 0; i < params.length; i++) {
- const rightHandNodes = this._pattern(params[i], PATTERN_DEFINE, scope);
- this._visitAll(rightHandNodes);
- }
- }
- const body = node.body;
- if (body.type === "BlockStatement") {
- // the body block is the function scope; it gets no scope of its own
- this._visitAll(body.body);
- } else {
- this._visit(body);
- }
- this._pop();
- if (named) this._pop();
- }
- /**
- * The class name is bound twice: outside, so siblings can see the class,
- * and inside, so the body and the heritage clause see a binding that an
- * outer reassignment cannot change.
- * @param {import("estree").ClassDeclaration | import("estree").ClassExpression} node Class node
- * @returns {void}
- */
- _visitClass(node) {
- if (node.type === "ClassDeclaration") {
- this._define(this.scope, /** @type {Node} */ (node.id));
- }
- const scope = this._push("class", node, false);
- if (node.id !== null && node.id !== undefined) {
- this._define(scope, node.id);
- }
- // the heritage clause is evaluated inside the class scope
- if (node.superClass !== null && node.superClass !== undefined) {
- this._visit(node.superClass);
- }
- this._visit(node.body);
- this._pop();
- }
- /**
- * @param {import("estree").ForInStatement | import("estree").ForOfStatement} node the loop
- * @returns {void}
- */
- _visitForIn(node) {
- const left = node.left;
- const lexical = left.type === "VariableDeclaration" && left.kind !== "var";
- if (lexical) this._push("for", node, false);
- if (left.type === "VariableDeclaration") {
- this._visit(left);
- // the loop head writes each iteration; right-hand nodes were
- // already visited by the declaration above
- this._pattern(
- left.declarations[0].id,
- PATTERN_REFERENCE_PLAIN,
- this.scope
- );
- } else {
- const rightHandNodes = this._pattern(left, PATTERN_REFERENCE, this.scope);
- this._visitAll(rightHandNodes);
- }
- this._visit(node.right);
- this._visit(node.body);
- if (lexical) this._pop();
- }
- /**
- * Fallback for node types the key table does not know, so unfamiliar
- * syntax still contributes its references instead of silently vanishing.
- * @param {Node} node node of an unknown type
- * @returns {void}
- */
- _visitUnknown(node) {
- for (const key in node) {
- if (
- key === "type" ||
- key === "start" ||
- key === "end" ||
- key === "range" ||
- key === "loc" ||
- key === "parent" ||
- key === "leadingComments" ||
- key === "trailingComments"
- ) {
- continue;
- }
- if (key === "key" && "computed" in node && node.computed === false) {
- continue;
- }
- const child = /** @type {Record<string, unknown>} */ (
- /** @type {unknown} */ (node)
- )[key];
- if (child === null || typeof child !== "object") continue;
- if (Array.isArray(child)) {
- for (const item of child) {
- if (item !== null && typeof item === "object" && item.type) {
- this._visit(item);
- }
- }
- } else if (/** @type {Node} */ (child).type) {
- this._visit(/** @type {Node} */ (child));
- }
- }
- }
- /**
- * @param {Node} node node to visit
- * @returns {void}
- */
- _visit(node) {
- // cases are ordered by measured frequency: the chain is a sequence of
- // comparisons, so a type that falls through to `default` pays all of them
- switch (node.type) {
- case "Identifier":
- this._reference(node);
- return;
- // leaves too common to leave at the end of the chain: together they
- // are ~14% of all visits, and every case above them is a comparison
- case "Literal":
- case "ThisExpression":
- return;
- case "MemberExpression":
- this._visit(node.object);
- // `a.b` reads `a`; `b` is a property name, not a binding
- if (node.computed) this._visit(node.property);
- return;
- // the types the key table below would otherwise handle, and which
- // together are ~31% of all visits — a call alone is 10%
- case "CallExpression":
- case "NewExpression":
- this._visit(node.callee);
- this._visitAll(node.arguments);
- return;
- case "ExpressionStatement":
- this._visit(node.expression);
- return;
- case "BinaryExpression":
- case "LogicalExpression":
- this._visit(node.left);
- this._visit(node.right);
- return;
- case "ReturnStatement":
- case "ThrowStatement":
- case "UnaryExpression":
- case "AwaitExpression":
- case "SpreadElement":
- case "YieldExpression":
- if (node.argument !== null && node.argument !== undefined) {
- this._visit(node.argument);
- }
- return;
- case "IfStatement":
- case "ConditionalExpression":
- this._visit(node.test);
- this._visit(node.consequent);
- if (node.alternate !== null && node.alternate !== undefined) {
- this._visit(node.alternate);
- }
- return;
- case "SwitchCase":
- if (node.test !== null && node.test !== undefined) {
- this._visit(node.test);
- }
- this._visitAll(node.consequent);
- return;
- case "ArrayExpression":
- this._visitAll(node.elements);
- return;
- case "ObjectExpression":
- this._visitAll(node.properties);
- return;
- case "Property":
- case "MethodDefinition":
- if (node.computed) this._visit(node.key);
- this._visit(node.value);
- return;
- case "PropertyDefinition":
- if (node.computed) this._visit(node.key);
- if (node.value !== null && node.value !== undefined) {
- // each field initializer runs in its own scope
- this._push("class-field-initializer", node.value, true);
- this._visit(node.value);
- this._pop();
- }
- return;
- case "StaticBlock":
- this._push("class-static-block", node, true);
- this._visitAll(node.body);
- this._pop();
- return;
- case "BlockStatement": {
- const scoped = needsScope(node.body);
- if (scoped) this._push("block", node, false);
- this._visitAll(node.body);
- if (scoped) this._pop();
- return;
- }
- case "SwitchStatement": {
- this._visit(node.discriminant);
- const scoped = needsScope(node.cases);
- if (scoped) this._push("switch", node, false);
- this._visitAll(node.cases);
- if (scoped) this._pop();
- return;
- }
- case "ForStatement": {
- const init = node.init;
- const lexical =
- init !== null &&
- init !== undefined &&
- init.type === "VariableDeclaration" &&
- init.kind !== "var";
- if (lexical) this._push("for", node, false);
- if (init !== null && init !== undefined) this._visit(init);
- if (node.test !== null && node.test !== undefined) {
- this._visit(node.test);
- }
- if (node.update !== null && node.update !== undefined) {
- this._visit(node.update);
- }
- this._visit(node.body);
- if (lexical) this._pop();
- return;
- }
- case "ForInStatement":
- case "ForOfStatement":
- this._visitForIn(node);
- return;
- case "VariableDeclaration": {
- // `var` hoists to the nearest function-like scope, everything
- // else binds right here
- const target =
- node.kind === "var" ? this.scope.variableScope : this.scope;
- for (const declarator of node.declarations) {
- const init = declarator.init;
- const initialized = init !== null && init !== undefined;
- const rightHandNodes = this._pattern(
- declarator.id,
- initialized ? PATTERN_DEFINE_INIT : PATTERN_DEFINE,
- target
- );
- this._visitAll(rightHandNodes);
- if (initialized) this._visit(init);
- }
- return;
- }
- case "AssignmentExpression":
- if (isPattern(node.left)) {
- if (node.operator === "=") {
- const rightHandNodes = this._pattern(
- node.left,
- PATTERN_REFERENCE,
- this.scope
- );
- this._visitAll(rightHandNodes);
- } else if (node.left.type === "Identifier") {
- // `x += 1` reads and writes the same binding
- this._reference(node.left);
- } else {
- this._visit(node.left);
- }
- } else {
- this._visit(node.left);
- }
- this._visit(node.right);
- return;
- case "UpdateExpression":
- if (node.argument.type === "Identifier") {
- this._reference(node.argument);
- } else {
- this._visit(node.argument);
- }
- return;
- case "FunctionDeclaration":
- case "FunctionExpression":
- case "ArrowFunctionExpression":
- this._visitFunction(node);
- return;
- case "ClassDeclaration":
- case "ClassExpression":
- this._visitClass(node);
- return;
- case "CatchClause":
- this._push("catch", node, false);
- if (node.param !== null && node.param !== undefined) {
- const rightHandNodes = this._pattern(
- node.param,
- PATTERN_DEFINE,
- this.scope
- );
- this._visitAll(rightHandNodes);
- }
- this._visit(node.body);
- this._pop();
- return;
- case "WithStatement":
- this._visit(node.object);
- this._push("with", node, false);
- this._visit(node.body);
- this._pop();
- return;
- case "ImportDeclaration":
- // every specifier introduces a local binding; the source is a
- // literal and attributes hold no references
- for (const specifier of node.specifiers) {
- const local = specifier.local;
- if (local !== null && local !== undefined) {
- this._define(this.scope, local);
- }
- }
- return;
- case "ExportAllDeclaration":
- // always re-exports from a source, so nothing local is referenced
- return;
- case "ExportDefaultDeclaration":
- this._visit(/** @type {Node} */ (node.declaration));
- return;
- case "ExportNamedDeclaration":
- if (node.source !== null && node.source !== undefined) return;
- if (node.declaration !== null && node.declaration !== undefined) {
- this._visit(node.declaration);
- return;
- }
- this._visitAll(node.specifiers);
- return;
- case "ExportSpecifier":
- // `export { x }` reads `x`; the exported name is not a binding
- if (node.local.type === "Identifier") this._reference(node.local);
- return;
- case "LabeledStatement":
- // labels share the identifier node type but are not bindings
- this._visit(node.body);
- return;
- case "BreakStatement":
- case "ContinueStatement":
- case "MetaProperty":
- case "PrivateIdentifier":
- case "Super":
- case "EmptyStatement":
- case "DebuggerStatement":
- return;
- default: {
- const keys = CHILD_KEYS[node.type];
- if (keys === undefined) {
- this._visitUnknown(node);
- return;
- }
- for (let i = 0; i < keys.length; i++) {
- const child = /** @type {NodeChildren} */ (
- /** @type {unknown} */ (node)
- )[keys[i]];
- if (child === null || child === undefined) continue;
- if (Array.isArray(child)) {
- for (let j = 0; j < child.length; j++) {
- const item = child[j];
- if (item !== null && item !== undefined) this._visit(item);
- }
- } else {
- this._visit(child);
- }
- }
- }
- }
- }
- /**
- * Resolves every recorded reference by climbing the scope chain from where
- * it was seen. Runs once, after the whole tree has been walked, so a
- * reference to a binding declared later still finds it.
- * @returns {Reference[]} references that resolved to no binding
- */
- _resolve() {
- const identifiers = this.pendingIdentifiers;
- const scopes = this.pendingScopes;
- /** @type {Reference[]} */
- const unresolved = [];
- for (let i = 0; i < identifiers.length; i++) {
- const identifier = identifiers[i];
- const name = identifier.name;
- const from = scopes[i];
- /** @type {Scope | null} */
- let scope = from;
- let resolved = false;
- while (scope !== null) {
- const variable = scope.getBinding(name);
- if (variable !== undefined) {
- // `-1` is every scope but a function scope with parameters, so
- // the common case is one integer compare and no call
- const boundary = scope.paramBoundary;
- if (
- boundary === -1 ||
- !isHiddenBodyBinding(variable, identifier, boundary)
- ) {
- // most identifiers resolve into a scope nothing reads back,
- // so the `Reference` is built only where one is kept
- if (scope._recorded) {
- const reference = new Reference(identifier, from);
- reference.resolved = variable;
- if (variable.references === NO_REFERENCES) {
- variable.references = [reference];
- } else {
- variable.references.push(reference);
- }
- }
- resolved = true;
- break;
- }
- }
- scope = scope.upper;
- }
- if (!resolved) unresolved.push(new Reference(identifier, from));
- }
- return unresolved;
- }
- }
- /**
- * @typedef {object} ScopeAnalysis
- * @property {Scope} globalScope the outermost scope
- * @property {Scope} moduleScope the module body scope, where top-level declarations live
- * @property {Reference[]} unresolvedReferences every identifier that resolved to no binding — the module's free names
- */
- /**
- * Analyses a generated module source as a strict ES module. Only the module
- * scope and its direct children collect references; `recordEveryReference`
- * widens that to the whole tree, retaining one per identifier.
- * @param {Program} ast the program to analyse
- * @param {boolean=} recordEveryReference whether every binding collects its references
- * @returns {ScopeAnalysis} the scope tree and the module's free references
- */
- const analyzeScope = (ast, recordEveryReference = false) => {
- const analyzer = new ScopeAnalyzer(recordEveryReference);
- const globalScope = analyzer._push("global", ast, true);
- const moduleScope = analyzer._push("module", ast, true);
- analyzer._visitAll(ast.body);
- const unresolvedReferences = analyzer._resolve();
- return { globalScope, moduleScope, unresolvedReferences };
- };
- analyzeScope.Reference = Reference;
- analyzeScope.Scope = Scope;
- analyzeScope.Variable = Variable;
- module.exports = analyzeScope;
|