2025-09-05 14:59:21 +08:00

446 lines
13 KiB
JavaScript

import { walk as walk$1 } from 'estree-walker';
import { createRegExp, exactly, anyOf } from 'magic-regexp/further-magic';
import { parseSync } from 'oxc-parser';
class ScopeTracker {
scopeIndexStack = [];
scopeIndexKey = "";
scopes = /* @__PURE__ */ new Map();
options;
isFrozen = false;
constructor(options = {}) {
this.options = options;
}
updateScopeIndexKey() {
this.scopeIndexKey = this.scopeIndexStack.slice(0, -1).join("-");
}
pushScope() {
this.scopeIndexStack.push(0);
this.updateScopeIndexKey();
}
popScope() {
this.scopeIndexStack.pop();
if (this.scopeIndexStack[this.scopeIndexStack.length - 1] !== void 0) {
this.scopeIndexStack[this.scopeIndexStack.length - 1]++;
}
if (!this.options.preserveExitedScopes) {
this.scopes.delete(this.scopeIndexKey);
}
this.updateScopeIndexKey();
}
declareIdentifier(name, data) {
if (this.isFrozen) {
return;
}
let scope = this.scopes.get(this.scopeIndexKey);
if (!scope) {
scope = /* @__PURE__ */ new Map();
this.scopes.set(this.scopeIndexKey, scope);
}
scope.set(name, data);
}
declareFunctionParameter(param, fn) {
if (this.isFrozen) {
return;
}
const identifiers = getPatternIdentifiers(param);
for (const identifier of identifiers) {
this.declareIdentifier(identifier.name, new ScopeTrackerFunctionParam(identifier, this.scopeIndexKey, fn));
}
}
declarePattern(pattern, parent) {
if (this.isFrozen) {
return;
}
const identifiers = getPatternIdentifiers(pattern);
for (const identifier of identifiers) {
this.declareIdentifier(
identifier.name,
parent.type === "VariableDeclaration" ? new ScopeTrackerVariable(identifier, this.scopeIndexKey, parent) : parent.type === "CatchClause" ? new ScopeTrackerCatchParam(identifier, this.scopeIndexKey, parent) : new ScopeTrackerFunctionParam(identifier, this.scopeIndexKey, parent)
);
}
}
processNodeEnter(node) {
switch (node.type) {
case "Program":
case "BlockStatement":
case "StaticBlock":
this.pushScope();
break;
case "FunctionDeclaration":
if (node.id?.name) {
this.declareIdentifier(node.id.name, new ScopeTrackerFunction(node, this.scopeIndexKey));
}
this.pushScope();
for (const param of node.params) {
this.declareFunctionParameter(param, node);
}
break;
case "FunctionExpression":
this.pushScope();
if (node.id?.name) {
this.declareIdentifier(node.id.name, new ScopeTrackerFunction(node, this.scopeIndexKey));
}
this.pushScope();
for (const param of node.params) {
this.declareFunctionParameter(param, node);
}
break;
case "ArrowFunctionExpression":
this.pushScope();
for (const param of node.params) {
this.declareFunctionParameter(param, node);
}
break;
case "VariableDeclaration":
for (const decl of node.declarations) {
this.declarePattern(decl.id, node);
}
break;
case "ClassDeclaration":
if (node.id?.name) {
this.declareIdentifier(node.id.name, new ScopeTrackerIdentifier(node.id, this.scopeIndexKey));
}
break;
case "ClassExpression":
this.pushScope();
if (node.id?.name) {
this.declareIdentifier(node.id.name, new ScopeTrackerIdentifier(node.id, this.scopeIndexKey));
}
break;
case "ImportDeclaration":
for (const specifier of node.specifiers) {
this.declareIdentifier(specifier.local.name, new ScopeTrackerImport(specifier, this.scopeIndexKey, node));
}
break;
case "CatchClause":
this.pushScope();
if (node.param) {
this.declarePattern(node.param, node);
}
break;
case "ForStatement":
case "ForOfStatement":
case "ForInStatement":
this.pushScope();
if (node.type === "ForStatement" && node.init?.type === "VariableDeclaration") {
for (const decl of node.init.declarations) {
this.declarePattern(decl.id, node.init);
}
} else if ((node.type === "ForOfStatement" || node.type === "ForInStatement") && node.left.type === "VariableDeclaration") {
for (const decl of node.left.declarations) {
this.declarePattern(decl.id, node.left);
}
}
break;
}
}
processNodeLeave(node) {
switch (node.type) {
case "Program":
case "BlockStatement":
case "CatchClause":
case "FunctionDeclaration":
case "ArrowFunctionExpression":
case "StaticBlock":
case "ClassExpression":
case "ForStatement":
case "ForOfStatement":
case "ForInStatement":
this.popScope();
break;
case "FunctionExpression":
this.popScope();
this.popScope();
break;
}
}
/**
* Check if an identifier is declared in the current scope or any parent scope.
* @param name the identifier name to check
*/
isDeclared(name) {
if (!this.scopeIndexKey) {
return this.scopes.get("")?.has(name) || false;
}
const indices = this.scopeIndexKey.split("-").map(Number);
for (let i = indices.length; i >= 0; i--) {
if (this.scopes.get(indices.slice(0, i).join("-"))?.has(name)) {
return true;
}
}
return false;
}
/**
* Get the declaration node for a given identifier name.
* @param name the identifier name to look up
*/
getDeclaration(name) {
if (!this.scopeIndexKey) {
return this.scopes.get("")?.get(name) ?? null;
}
const indices = this.scopeIndexKey.split("-").map(Number);
for (let i = indices.length; i >= 0; i--) {
const node = this.scopes.get(indices.slice(0, i).join("-"))?.get(name);
if (node) {
return node;
}
}
return null;
}
/**
* Get the current scope key.
*/
getCurrentScope() {
return this.scopeIndexKey;
}
/**
* Check if the current scope is a child of a specific scope.
* @example
* ```ts
* // current scope is 0-1
* isCurrentScopeUnder('0') // true
* isCurrentScopeUnder('0-1') // false
* ```
*
* @param scope the parent scope key to check against
* @returns `true` if the current scope is a child of the specified scope, `false` otherwise (also when they are the same)
*/
isCurrentScopeUnder(scope) {
return isChildScope(this.scopeIndexKey, scope);
}
/**
* Freezes the ScopeTracker, preventing further modifications to its state.
* It also resets the scope index stack to its initial state so that the tracker can be reused.
*
* This is useful for second passes through the AST.
*/
freeze() {
this.isFrozen = true;
this.scopeIndexStack = [];
this.updateScopeIndexKey();
}
}
function getPatternIdentifiers(pattern) {
const identifiers = [];
function collectIdentifiers(pattern2) {
switch (pattern2.type) {
case "Identifier":
identifiers.push(pattern2);
break;
case "AssignmentPattern":
collectIdentifiers(pattern2.left);
break;
case "RestElement":
collectIdentifiers(pattern2.argument);
break;
case "ArrayPattern":
for (const element of pattern2.elements) {
if (element) {
collectIdentifiers(element.type === "RestElement" ? element.argument : element);
}
}
break;
case "ObjectPattern":
for (const property of pattern2.properties) {
collectIdentifiers(property.type === "RestElement" ? property.argument : property.value);
}
break;
}
}
collectIdentifiers(pattern);
return identifiers;
}
function isBindingIdentifier(node, parent) {
if (!parent || node.type !== "Identifier") {
return false;
}
switch (parent.type) {
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
if (parent.type !== "ArrowFunctionExpression" && parent.id === node) {
return true;
}
if (parent.params.length) {
for (const param of parent.params) {
const identifiers = getPatternIdentifiers(param);
if (identifiers.includes(node)) {
return true;
}
}
}
return false;
case "ClassDeclaration":
case "ClassExpression":
return parent.id === node;
case "MethodDefinition":
return parent.key === node;
case "PropertyDefinition":
return parent.key === node;
case "VariableDeclarator":
return getPatternIdentifiers(parent.id).includes(node);
case "CatchClause":
if (!parent.param) {
return false;
}
return getPatternIdentifiers(parent.param).includes(node);
case "Property":
return parent.key === node && parent.value !== node;
case "MemberExpression":
return parent.property === node;
}
return false;
}
function getUndeclaredIdentifiersInFunction(node) {
const scopeTracker = new ScopeTracker({
preserveExitedScopes: true
});
const undeclaredIdentifiers = /* @__PURE__ */ new Set();
function isIdentifierUndeclared(node2, parent) {
return !isBindingIdentifier(node2, parent) && !scopeTracker.isDeclared(node2.name);
}
walk(node, {
scopeTracker
});
scopeTracker.freeze();
walk(node, {
scopeTracker,
enter(node2, parent) {
if (node2.type === "Identifier" && isIdentifierUndeclared(node2, parent)) {
undeclaredIdentifiers.add(node2.name);
}
}
});
return Array.from(undeclaredIdentifiers);
}
function isChildScope(a, b) {
return a.startsWith(b) && a.length > b.length;
}
class BaseNode {
scope;
node;
constructor(node, scope) {
this.node = node;
this.scope = scope;
}
/**
* Check if the node is defined under a specific scope.
* @param scope
*/
isUnderScope(scope) {
return isChildScope(this.scope, scope);
}
}
class ScopeTrackerIdentifier extends BaseNode {
type = "Identifier";
get start() {
return this.node.start;
}
get end() {
return this.node.end;
}
}
class ScopeTrackerFunctionParam extends BaseNode {
type = "FunctionParam";
fnNode;
constructor(node, scope, fnNode) {
super(node, scope);
this.fnNode = fnNode;
}
/**
* @deprecated The representation of this position may change in the future. Use `.fnNode.start` instead for now.
*/
get start() {
return this.fnNode.start;
}
/**
* @deprecated The representation of this position may change in the future. Use `.fnNode.end` instead for now.
*/
get end() {
return this.fnNode.end;
}
}
class ScopeTrackerFunction extends BaseNode {
type = "Function";
get start() {
return this.node.start;
}
get end() {
return this.node.end;
}
}
class ScopeTrackerVariable extends BaseNode {
type = "Variable";
variableNode;
constructor(node, scope, variableNode) {
super(node, scope);
this.variableNode = variableNode;
}
get start() {
return this.variableNode.start;
}
get end() {
return this.variableNode.end;
}
}
class ScopeTrackerImport extends BaseNode {
type = "Import";
importNode;
constructor(node, scope, importNode) {
super(node, scope);
this.importNode = importNode;
}
get start() {
return this.importNode.start;
}
get end() {
return this.importNode.end;
}
}
class ScopeTrackerCatchParam extends BaseNode {
type = "CatchParam";
catchNode;
constructor(node, scope, catchNode) {
super(node, scope);
this.catchNode = catchNode;
}
get start() {
return this.catchNode.start;
}
get end() {
return this.catchNode.end;
}
}
function walk(input, options) {
const scopeTracker = options.scopeTracker;
return walk$1(
input,
{
enter(node, parent, key, index) {
scopeTracker?.processNodeEnter(node);
options.enter?.call(this, node, parent, { key, index, ast: input });
},
leave(node, parent, key, index) {
scopeTracker?.processNodeLeave(node);
options.leave?.call(this, node, parent, { key, index, ast: input });
}
}
);
}
const LANG_RE = createRegExp(exactly("jsx").or("tsx").or("js").or("ts").groupedAs("lang").after(exactly(".").and(anyOf("c", "m").optionally())).at.lineEnd());
function parseAndWalk(code, sourceFilename, arg3) {
const lang = sourceFilename?.match(LANG_RE)?.groups?.lang;
const {
parseOptions: _parseOptions = {},
...options
} = typeof arg3 === "function" ? { enter: arg3 } : arg3;
const parseOptions = { sourceType: "module", lang, ..._parseOptions };
const ast = parseSync(sourceFilename, code, parseOptions);
walk(ast.program, options);
return ast;
}
export { ScopeTracker, getUndeclaredIdentifiersInFunction, isBindingIdentifier, parseAndWalk, walk };