873 lines
30 KiB
JavaScript
873 lines
30 KiB
JavaScript
import { createRequire } from "node:module";
|
||
//#region src/utils.ts
|
||
function isNode(v) {
|
||
return v !== null && typeof v === "object" && typeof v.type === "string";
|
||
}
|
||
//#endregion
|
||
//#region src/walker/base.ts
|
||
var WalkerBase = class {
|
||
scopeTracker;
|
||
enter;
|
||
leave;
|
||
contextEnter = {
|
||
skip: () => {
|
||
this._skip = true;
|
||
},
|
||
remove: () => {
|
||
this._remove = true;
|
||
},
|
||
replace: (node) => {
|
||
this._replacement = node;
|
||
}
|
||
};
|
||
contextLeave = {
|
||
remove: this.contextEnter.remove,
|
||
replace: this.contextEnter.replace
|
||
};
|
||
_skip = false;
|
||
_remove = false;
|
||
_replacement = null;
|
||
constructor(handler, options) {
|
||
this.enter = handler.enter;
|
||
this.leave = handler.leave;
|
||
this.scopeTracker = options?.scopeTracker;
|
||
}
|
||
replace(parent, key, index, node) {
|
||
if (!parent || key === null) return;
|
||
if (index !== null) parent[key][index] = node;
|
||
else parent[key] = node;
|
||
}
|
||
insert(parent, key, index, node) {
|
||
if (!parent || key === null) return;
|
||
if (index !== null) parent[key].splice(index, 0, node);
|
||
else parent[key] = node;
|
||
}
|
||
remove(parent, key, index) {
|
||
if (!parent || key === null) return;
|
||
if (index !== null) parent[key].splice(index, 1);
|
||
else delete parent[key];
|
||
}
|
||
};
|
||
//#endregion
|
||
//#region src/walker/sync.ts
|
||
var WalkerSync = class extends WalkerBase {
|
||
constructor(handler, options) {
|
||
super(handler, options);
|
||
}
|
||
traverse(input, key, index, parent) {
|
||
const ctx = {
|
||
key: null,
|
||
index: index ?? null,
|
||
ast: input
|
||
};
|
||
const scopeTracker = this.scopeTracker;
|
||
const enter = this.enter;
|
||
const leave = this.leave;
|
||
const contextEnter = this.contextEnter;
|
||
const contextLeave = this.contextLeave;
|
||
const _walk = (input, parent, key, index, skip) => {
|
||
if (scopeTracker) scopeTracker.processNodeEnter(input);
|
||
let currentNode = input;
|
||
let removedInEnter = false;
|
||
let replacedInEnter = false;
|
||
let skipChildren = skip;
|
||
if (enter && !skip) {
|
||
const _skip = this._skip;
|
||
const _remove = this._remove;
|
||
const _replacement = this._replacement;
|
||
this._skip = false;
|
||
this._remove = false;
|
||
this._replacement = null;
|
||
ctx.key = key;
|
||
ctx.index = index;
|
||
enter.call(contextEnter, input, parent, ctx);
|
||
if (this._replacement && !this._remove) {
|
||
currentNode = this._replacement;
|
||
replacedInEnter = true;
|
||
this.replace(parent, key, index, this._replacement);
|
||
if (scopeTracker) scopeTracker.processNodeEnter(currentNode);
|
||
}
|
||
if (this._remove) {
|
||
removedInEnter = true;
|
||
currentNode = null;
|
||
this.remove(parent, key, index);
|
||
}
|
||
if (this._skip) skipChildren = true;
|
||
this._skip = _skip;
|
||
this._remove = _remove;
|
||
this._replacement = _replacement;
|
||
}
|
||
if ((!skipChildren || scopeTracker) && currentNode) for (const k in currentNode) {
|
||
if (k === "type" || k === "start" || k === "end") continue;
|
||
const node = currentNode[k];
|
||
if (!node || typeof node !== "object") continue;
|
||
if (Array.isArray(node)) for (let i = 0; i < node.length; i++) {
|
||
const child = node[i];
|
||
if (isNode(child)) {
|
||
if (_walk(child, currentNode, k, i, skipChildren) === null) i--;
|
||
}
|
||
}
|
||
else if (typeof node.type === "string") _walk(node, currentNode, k, null, skipChildren);
|
||
}
|
||
if (scopeTracker) {
|
||
if (replacedInEnter) scopeTracker.processNodeLeave(currentNode);
|
||
scopeTracker.processNodeLeave(input);
|
||
}
|
||
if (leave && !skip) {
|
||
const _replacement = this._replacement;
|
||
const _remove = this._remove;
|
||
this._replacement = null;
|
||
this._remove = false;
|
||
ctx.key = key;
|
||
ctx.index = index;
|
||
leave.call(contextLeave, input, parent, ctx);
|
||
if (this._replacement && !this._remove) {
|
||
currentNode = this._replacement;
|
||
if (removedInEnter) this.insert(parent, key, index, this._replacement);
|
||
else this.replace(parent, key, index, this._replacement);
|
||
}
|
||
if (this._remove) {
|
||
currentNode = null;
|
||
this.remove(parent, key, index);
|
||
}
|
||
this._replacement = _replacement;
|
||
this._remove = _remove;
|
||
}
|
||
return currentNode;
|
||
};
|
||
if (!isNode(input)) return null;
|
||
if (scopeTracker) scopeTracker.onWalkStart(input);
|
||
if (scopeTracker && !enter && !leave) {
|
||
const _walkScopesOnly = (input) => {
|
||
scopeTracker.processNodeEnter(input);
|
||
for (const k in input) {
|
||
if (k === "type" || k === "start" || k === "end") continue;
|
||
const node = input[k];
|
||
if (!node || typeof node !== "object") continue;
|
||
if (Array.isArray(node)) for (let i = 0; i < node.length; i++) {
|
||
const child = node[i];
|
||
if (isNode(child)) _walkScopesOnly(child);
|
||
}
|
||
else if (typeof node.type === "string") _walkScopesOnly(node);
|
||
}
|
||
scopeTracker.processNodeLeave(input);
|
||
};
|
||
_walkScopesOnly(input);
|
||
return input;
|
||
}
|
||
return _walk(input, parent ?? null, key ?? null, index ?? null, false);
|
||
}
|
||
};
|
||
//#endregion
|
||
//#region src/walk.ts
|
||
let cachedParseSync;
|
||
function resolveParseSync() {
|
||
if (cachedParseSync) return cachedParseSync;
|
||
const require = createRequire(import.meta.url);
|
||
for (const id of ["oxc-parser", "rolldown/utils"]) try {
|
||
const mod = require(id);
|
||
if (typeof mod.parseSync === "function") {
|
||
cachedParseSync = mod.parseSync;
|
||
return cachedParseSync;
|
||
}
|
||
} catch {}
|
||
throw new Error("oxc-walker: could not resolve a `parseSync` implementation. Install `oxc-parser` or `rolldown` (and use `rolldown/utils`), or pass a `parseSync` function via the `parseAndWalk` options.");
|
||
}
|
||
/**
|
||
* Walk the AST with the given options.
|
||
* @param input The AST to walk.
|
||
* @param options The options to be used when walking the AST. Here you can specify the callbacks for entering and leaving nodes, as well as other options.
|
||
*/
|
||
function walk(input, options) {
|
||
return new WalkerSync({
|
||
enter: options.enter,
|
||
leave: options.leave
|
||
}, { scopeTracker: options.scopeTracker }).traverse(input);
|
||
}
|
||
const LANG_RE = /\.(?:c|m)?(?<lang>jsx?|tsx?)$/;
|
||
function parseAndWalk(code, sourceFilename, arg3) {
|
||
const lang = sourceFilename?.match(LANG_RE)?.groups?.lang;
|
||
const { parseOptions: _parseOptions = {}, parseSync: _parseSync, ...options } = typeof arg3 === "function" ? { enter: arg3 } : arg3;
|
||
const parseOptions = {
|
||
sourceType: "module",
|
||
lang,
|
||
..._parseOptions
|
||
};
|
||
const ast = (_parseSync ?? resolveParseSync())(sourceFilename, code, parseOptions);
|
||
walk(ast.program, options);
|
||
return ast;
|
||
}
|
||
//#endregion
|
||
//#region src/scope-tracker.ts
|
||
/**
|
||
* The node types handled by {@link ScopeTracker.processNodeEnter}.
|
||
* Must list every `case` of its switch statement; a missing entry silently disables
|
||
* scope tracking for that node type (see the drift test in `test/scope-tracker.test.ts`).
|
||
*/
|
||
const SCOPE_ENTER_TYPES = /* @__PURE__ */ new Set([
|
||
"Program",
|
||
"BlockStatement",
|
||
"StaticBlock",
|
||
"TSModuleBlock",
|
||
"FunctionDeclaration",
|
||
"FunctionExpression",
|
||
"ArrowFunctionExpression",
|
||
"VariableDeclaration",
|
||
"ClassDeclaration",
|
||
"ClassExpression",
|
||
"ImportDeclaration",
|
||
"TSEnumDeclaration",
|
||
"TSModuleDeclaration",
|
||
"TSImportEqualsDeclaration",
|
||
"TSDeclareFunction",
|
||
"TSInterfaceDeclaration",
|
||
"TSTypeAliasDeclaration",
|
||
"TSTypeParameter",
|
||
"TSMappedType",
|
||
"TSEnumBody",
|
||
"TSMethodSignature",
|
||
"TSCallSignatureDeclaration",
|
||
"TSConstructSignatureDeclaration",
|
||
"TSFunctionType",
|
||
"TSConstructorType",
|
||
"TSConditionalType",
|
||
"CatchClause",
|
||
"ForStatement",
|
||
"ForOfStatement",
|
||
"ForInStatement",
|
||
"SwitchStatement"
|
||
]);
|
||
/**
|
||
* Tracks variable scopes and identifier declarations within a JavaScript AST.
|
||
*
|
||
* Maintains a stack of scopes, each represented as a map from identifier names to their declaration nodes,
|
||
* enabling efficient lookup of the declaration.
|
||
*
|
||
* The ScopeTracker is designed to integrate with the `walk` function,
|
||
* it automatically manages scope creation and identifier tracking,
|
||
* so only query and inspection methods are exposed for external use.
|
||
*
|
||
* ### Scope tracking
|
||
* A new scope is created when entering blocks, function parameters, loop variables, etc.
|
||
* Note that this representation may split a single JavaScript lexical scope into multiple internal scopes,
|
||
* meaning it doesn't mirror JavaScript’s scoping 1:1.
|
||
*
|
||
* Scopes are represented using a string-based index like `"0-1-2"`, which tracks depth and ancestry.
|
||
*
|
||
* #### Root scope
|
||
* The root scope is represented by an empty string `""`.
|
||
*
|
||
* #### Scope key format
|
||
* Scope keys are hierarchical strings that uniquely identify each scope and its position in the tree.
|
||
* They are constructed using a depth-based indexing scheme, where:
|
||
*
|
||
* - the root scope is represented by an empty string `""`.
|
||
* - the first child scope is `"0"`.
|
||
* - a parallel sibling of `"0"` becomes `"1"`, `"2"`, etc.
|
||
* - a nested scope under `"0"` is `"0-0"`, then its sibling is `"0-1"`, and so on.
|
||
*
|
||
* Each segment in the key corresponds to the zero-based index of the scope at that depth level in
|
||
* the order of AST traversal.
|
||
*
|
||
* ### Additional features
|
||
* - supports freezing the tracker to allow for second passes through the AST without modifying the scope data
|
||
* (useful for doing a pre-pass to collect all identifiers before walking).
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* const scopeTracker = new ScopeTracker()
|
||
* walk(code, {
|
||
* scopeTracker,
|
||
* enter(node) {
|
||
* // ...
|
||
* },
|
||
* })
|
||
* ```
|
||
*
|
||
* @see parseAndWalk
|
||
* @see walk
|
||
*/
|
||
var ScopeTracker = class {
|
||
scopeIndexStack = [];
|
||
/**
|
||
* The scope keys of the ancestors of the current scope, one entry per active scope.
|
||
* `pushScope` pushes the current `scopeIndexKey` before deriving the new one,
|
||
* and `popScope` restores it from here, so the stack always mirrors the path
|
||
* from the root scope down to the parent of the current scope.
|
||
*/
|
||
scopeKeyStack = [];
|
||
/**
|
||
* The nodes that created the currently active scopes, one entry per scope.
|
||
* A node that creates multiple scopes (e.g. `FunctionExpression`) appears once per scope.
|
||
* This lets `processNodeLeave` detect scope ends with a pointer comparison
|
||
* instead of checking the node type.
|
||
*/
|
||
scopeOwnerStack = [];
|
||
scopeIndexKey = "";
|
||
scopes = /* @__PURE__ */ new Map();
|
||
typeScopes = /* @__PURE__ */ new Map();
|
||
/**
|
||
* The implicit `arguments` keyed by the function that introduces them,
|
||
* so that repeated queries return the same instance.
|
||
*/
|
||
implicitArguments = /* @__PURE__ */ new WeakMap();
|
||
options;
|
||
isFrozen = false;
|
||
constructor(options = {}) {
|
||
this.options = options;
|
||
}
|
||
onWalkStart = (root) => {
|
||
if (this.scopeIndexStack.length === 0 && root.type !== "Program") this.scopeIndexStack.push(0);
|
||
};
|
||
pushScope(owner) {
|
||
const depthIndex = this.scopeIndexStack[this.scopeIndexStack.length - 1];
|
||
this.scopeKeyStack.push(this.scopeIndexKey);
|
||
this.scopeOwnerStack.push(owner);
|
||
this.scopeIndexStack.push(0);
|
||
if (depthIndex !== void 0) this.scopeIndexKey = this.scopeIndexKey ? `${this.scopeIndexKey}-${depthIndex}` : `${depthIndex}`;
|
||
}
|
||
popScope() {
|
||
this.scopeOwnerStack.pop();
|
||
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.typeScopes.delete(this.scopeIndexKey);
|
||
}
|
||
this.scopeIndexKey = this.scopeKeyStack.pop() ?? "";
|
||
}
|
||
/**
|
||
* Get the key of the active scope at the given index of the scope stacks.
|
||
*/
|
||
getScopeKeyAt(index) {
|
||
return index === this.scopeKeyStack.length - 1 ? this.scopeIndexKey : this.scopeKeyStack[index + 1];
|
||
}
|
||
/**
|
||
* Get the key of the closest function-like scope (function bodies, static blocks,
|
||
* module blocks and the root scope), which is where `var` declarations are hoisted to.
|
||
*/
|
||
getVarScopeKey() {
|
||
const owners = this.scopeOwnerStack;
|
||
for (let i = owners.length - 1; i >= 0; i--) switch (owners[i].type) {
|
||
case "FunctionDeclaration":
|
||
case "FunctionExpression":
|
||
case "ArrowFunctionExpression":
|
||
case "TSDeclareFunction":
|
||
case "Program":
|
||
case "StaticBlock":
|
||
case "TSModuleBlock": return this.getScopeKeyAt(i);
|
||
}
|
||
return "";
|
||
}
|
||
declareInNamespace(scopes, name, node) {
|
||
let scope = scopes.get(node.scope);
|
||
if (!scope) {
|
||
scope = /* @__PURE__ */ new Map();
|
||
scopes.set(node.scope, scope);
|
||
}
|
||
scope.set(name, node);
|
||
}
|
||
declareIdentifier(name, data, namespaces = { value: true }) {
|
||
if (this.isFrozen) return;
|
||
if (namespaces.value) this.declareInNamespace(this.scopes, name, data);
|
||
if (namespaces.type) this.declareInNamespace(this.typeScopes, 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 scopeKey = parent.type === "VariableDeclaration" && parent.kind === "var" ? this.getVarScopeKey() : this.scopeIndexKey;
|
||
const identifiers = getPatternIdentifiers(pattern);
|
||
for (const identifier of identifiers) this.declareIdentifier(identifier.name, parent.type === "VariableDeclaration" ? new ScopeTrackerVariable(identifier, scopeKey, parent) : parent.type === "CatchClause" ? new ScopeTrackerCatchParam(identifier, scopeKey, parent) : new ScopeTrackerFunctionParam(identifier, scopeKey, parent));
|
||
}
|
||
processNodeEnter = (node) => {
|
||
if (!SCOPE_ENTER_TYPES.has(node.type)) return;
|
||
switch (node.type) {
|
||
case "Program":
|
||
case "BlockStatement":
|
||
case "StaticBlock":
|
||
case "SwitchStatement":
|
||
case "TSModuleBlock":
|
||
case "TSMethodSignature":
|
||
case "TSCallSignatureDeclaration":
|
||
case "TSConstructSignatureDeclaration":
|
||
case "TSFunctionType":
|
||
case "TSConstructorType":
|
||
case "TSConditionalType":
|
||
this.pushScope(node);
|
||
break;
|
||
case "FunctionDeclaration":
|
||
if (node.id?.name) this.declareIdentifier(node.id.name, new ScopeTrackerFunction(node, this.scopeIndexKey));
|
||
this.pushScope(node);
|
||
for (const param of node.params) this.declareFunctionParameter(param, node);
|
||
break;
|
||
case "FunctionExpression":
|
||
this.pushScope(node);
|
||
if (node.id?.name) this.declareIdentifier(node.id.name, new ScopeTrackerFunction(node, this.scopeIndexKey));
|
||
this.pushScope(node);
|
||
for (const param of node.params) this.declareFunctionParameter(param, node);
|
||
break;
|
||
case "ArrowFunctionExpression":
|
||
this.pushScope(node);
|
||
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), {
|
||
value: true,
|
||
type: true
|
||
});
|
||
if (node.typeParameters) this.pushScope(node);
|
||
break;
|
||
case "ClassExpression":
|
||
this.pushScope(node);
|
||
if (node.id?.name) this.declareIdentifier(node.id.name, new ScopeTrackerIdentifier(node.id, this.scopeIndexKey), {
|
||
value: true,
|
||
type: true
|
||
});
|
||
break;
|
||
case "ImportDeclaration":
|
||
for (const specifier of node.specifiers) {
|
||
const isTypeOnly = node.importKind === "type" || specifier.type === "ImportSpecifier" && specifier.importKind === "type";
|
||
this.declareIdentifier(specifier.local.name, new ScopeTrackerImport(specifier, this.scopeIndexKey, node), {
|
||
value: !isTypeOnly,
|
||
type: true
|
||
});
|
||
}
|
||
break;
|
||
case "TSEnumDeclaration":
|
||
case "TSModuleDeclaration":
|
||
case "TSImportEqualsDeclaration":
|
||
if (node.type === "TSModuleDeclaration" && node.kind === "global") break;
|
||
if (node.id?.type === "Identifier" && node.id.name) this.declareIdentifier(node.id.name, new ScopeTrackerIdentifier(node.id, this.scopeIndexKey), {
|
||
value: true,
|
||
type: true
|
||
});
|
||
break;
|
||
case "TSDeclareFunction":
|
||
if (node.id?.name) this.declareIdentifier(node.id.name, new ScopeTrackerIdentifier(node.id, this.scopeIndexKey));
|
||
this.pushScope(node);
|
||
break;
|
||
case "TSInterfaceDeclaration":
|
||
case "TSTypeAliasDeclaration":
|
||
if (node.id?.name) this.declareIdentifier(node.id.name, new ScopeTrackerIdentifier(node.id, this.scopeIndexKey), { type: true });
|
||
this.pushScope(node);
|
||
break;
|
||
case "TSTypeParameter":
|
||
if (node.name?.name) this.declareIdentifier(node.name.name, new ScopeTrackerIdentifier(node.name, this.scopeIndexKey), { type: true });
|
||
break;
|
||
case "TSMappedType":
|
||
this.pushScope(node);
|
||
this.declareIdentifier(node.key.name, new ScopeTrackerIdentifier(node.key, this.scopeIndexKey), { type: true });
|
||
break;
|
||
case "TSEnumBody":
|
||
this.pushScope(node);
|
||
for (const member of node.members) if (member.id.type === "Identifier") this.declareIdentifier(member.id.name, new ScopeTrackerIdentifier(member.id, this.scopeIndexKey));
|
||
break;
|
||
case "CatchClause":
|
||
this.pushScope(node);
|
||
if (node.param) this.declarePattern(node.param, node);
|
||
break;
|
||
case "ForStatement":
|
||
case "ForOfStatement":
|
||
case "ForInStatement":
|
||
this.pushScope(node);
|
||
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) => {
|
||
const owners = this.scopeOwnerStack;
|
||
while (owners.length > 0 && owners[owners.length - 1] === node) this.popScope();
|
||
};
|
||
getDeclarationIn(scopes, name) {
|
||
let key = this.scopeIndexKey;
|
||
while (true) {
|
||
const node = scopes.get(key)?.get(name);
|
||
if (node) return node;
|
||
if (!key) return null;
|
||
const separatorIndex = key.lastIndexOf("-");
|
||
key = separatorIndex === -1 ? "" : key.slice(0, separatorIndex);
|
||
}
|
||
}
|
||
/**
|
||
* Check if an identifier is declared in the current scope or any parent scope.
|
||
* @param name the identifier name to check
|
||
* @param options which namespace to check — the value namespace (default), the type namespace, or both
|
||
*/
|
||
isDeclared(name, options) {
|
||
return this.getDeclaration(name, options) !== null;
|
||
}
|
||
/**
|
||
* Get the declaration node for a given identifier name.
|
||
* @param name the identifier name to look up
|
||
* @param options which namespace to check — the value namespace (default), the type namespace, or both
|
||
*/
|
||
getDeclaration(name, options) {
|
||
const mode = options?.mode ?? "value";
|
||
const declaration = (mode !== "type" ? this.getDeclarationIn(this.scopes, name) : null) ?? (mode !== "value" ? this.getDeclarationIn(this.typeScopes, name) : null);
|
||
if (name === "arguments" && mode !== "type") {
|
||
const owners = this.scopeOwnerStack;
|
||
for (let i = owners.length - 1; i >= 0; i--) {
|
||
const owner = owners[i];
|
||
if (owner.type === "FunctionDeclaration" || owner.type === "FunctionExpression") {
|
||
const fnScope = this.getScopeKeyAt(i);
|
||
if (!declaration || isChildScope(fnScope, declaration.scope)) {
|
||
let implicit = this.implicitArguments.get(owner);
|
||
if (!implicit) {
|
||
implicit = new ScopeTrackerFunctionArguments(owner, fnScope);
|
||
this.implicitArguments.set(owner, implicit);
|
||
}
|
||
return implicit;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return declaration;
|
||
}
|
||
/**
|
||
* 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.scopeKeyStack = [];
|
||
this.scopeOwnerStack = [];
|
||
this.scopeIndexKey = "";
|
||
}
|
||
};
|
||
function getPatternIdentifiers(pattern) {
|
||
const identifiers = [];
|
||
function collectIdentifiers(pattern) {
|
||
switch (pattern.type) {
|
||
case "Identifier":
|
||
identifiers.push(pattern);
|
||
break;
|
||
case "AssignmentPattern":
|
||
collectIdentifiers(pattern.left);
|
||
break;
|
||
case "RestElement":
|
||
collectIdentifiers(pattern.argument);
|
||
break;
|
||
case "TSParameterProperty":
|
||
collectIdentifiers(pattern.parameter);
|
||
break;
|
||
case "ArrayPattern":
|
||
for (const element of pattern.elements) if (element) collectIdentifiers(element.type === "RestElement" ? element.argument : element);
|
||
break;
|
||
case "ObjectPattern":
|
||
for (const property of pattern.properties) collectIdentifiers(property.type === "RestElement" ? property.argument : property.value);
|
||
break;
|
||
}
|
||
}
|
||
collectIdentifiers(pattern);
|
||
return identifiers;
|
||
}
|
||
/**
|
||
* An allocation-free alternative to `getPatternIdentifiers(pattern).includes(node)`
|
||
*/
|
||
function isIdentifierInPattern(pattern, node) {
|
||
switch (pattern.type) {
|
||
case "Identifier": return pattern === node;
|
||
case "AssignmentPattern": return isIdentifierInPattern(pattern.left, node);
|
||
case "RestElement": return isIdentifierInPattern(pattern.argument, node);
|
||
case "TSParameterProperty": return isIdentifierInPattern(pattern.parameter, node);
|
||
case "ArrayPattern":
|
||
for (const element of pattern.elements) if (element && isIdentifierInPattern(element.type === "RestElement" ? element.argument : element, node)) return true;
|
||
return false;
|
||
case "ObjectPattern":
|
||
for (const property of pattern.properties) if (isIdentifierInPattern(property.type === "RestElement" ? property.argument : property.value, node)) return true;
|
||
return false;
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Check if an identifier is in a binding position, where it declares a new variable.
|
||
*
|
||
* Note that identifiers nested in destructuring patterns (`const { a, b = c } = obj`)
|
||
* cannot be resolved from the direct parent alone; this function returns `false` for them
|
||
* and {@link isReferenceIdentifier} reports them as references.
|
||
* Pair these functions with a {@link ScopeTracker} to resolve such identifiers correctly.
|
||
*/
|
||
function isOnlyBindingIdentifier(node, parent) {
|
||
if (!parent || node.type !== "Identifier") return false;
|
||
switch (parent.type) {
|
||
case "FunctionDeclaration":
|
||
case "FunctionExpression":
|
||
case "ArrowFunctionExpression":
|
||
case "TSDeclareFunction":
|
||
case "TSEmptyBodyFunctionExpression":
|
||
if (parent.type !== "ArrowFunctionExpression" && parent.id === node) return true;
|
||
for (const param of parent.params) if (isIdentifierInPattern(param, node)) return true;
|
||
return false;
|
||
case "ClassDeclaration":
|
||
case "ClassExpression": return parent.id === node;
|
||
case "VariableDeclarator": return isIdentifierInPattern(parent.id, node);
|
||
case "CatchClause":
|
||
if (!parent.param) return false;
|
||
return isIdentifierInPattern(parent.param, node);
|
||
case "ImportSpecifier": return parent.local === node;
|
||
case "ImportDefaultSpecifier":
|
||
case "ImportNamespaceSpecifier": return true;
|
||
case "TSEnumDeclaration":
|
||
case "TSModuleDeclaration": return parent.id === node;
|
||
case "TSImportEqualsDeclaration": return parent.id === node;
|
||
case "TSParameterProperty": return isIdentifierInPattern(parent.parameter, node);
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* @deprecated
|
||
* Despite its name, this function does not check for binding positions only.
|
||
* It will adopt the strict binding-position behavior of {@link isOnlyBindingIdentifier}
|
||
* in the next major version. Migrate based on what you need:
|
||
* - `!isReferenceIdentifier(node, parent)` if you were filtering out variable references
|
||
* (the common case)
|
||
* - `isOnlyBindingIdentifier(node, parent)` if you need actual binding positions
|
||
*/
|
||
function isBindingIdentifier(node, parent) {
|
||
if (!parent || node.type !== "Identifier") return false;
|
||
if (isOnlyBindingIdentifier(node, parent)) return true;
|
||
switch (parent.type) {
|
||
case "MethodDefinition":
|
||
case "PropertyDefinition": return parent.key === node;
|
||
case "Property": return parent.key === node && parent.value !== node;
|
||
case "MemberExpression": return parent.property === node;
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Check if an identifier is a reference.
|
||
*
|
||
* Note that this function returns `true` for the local name of a plain export
|
||
* (`export { foo }`), where it is a genuine reference to a local variable,
|
||
* but also for the local name of a re-export (`export { foo } from '...'`),
|
||
* where it refers to the other module's exports instead.
|
||
*
|
||
* The two are indistinguishable from the specifier alone, as the `source` lives
|
||
* on the parent `ExportNamedDeclaration`.
|
||
* Skip re-export declarations during the walk to avoid the false positives.
|
||
*
|
||
* Identifiers nested in destructuring patterns (`const { a, b = c } = obj`) are also
|
||
* indistinguishable from references based on the direct parent alone and are reported
|
||
* as references. Pair this function with a {@link ScopeTracker} to resolve them correctly.
|
||
*/
|
||
function isReferenceIdentifier(node, parent, options) {
|
||
if (!parent) return false;
|
||
const mode = options?.mode ?? "value";
|
||
if (node.type === "JSXIdentifier") {
|
||
if (mode === "type") return false;
|
||
switch (parent.type) {
|
||
case "JSXOpeningElement":
|
||
case "JSXClosingElement": return parent.name === node && !/^[a-z]/.test(node.name);
|
||
case "JSXMemberExpression": return parent.object === node;
|
||
case "JSXAttribute":
|
||
case "JSXNamespacedName": return false;
|
||
}
|
||
return false;
|
||
}
|
||
if (node.type !== "Identifier" || isOnlyBindingIdentifier(node, parent)) return false;
|
||
switch (parent.type) {
|
||
case "MemberExpression": return mode !== "type" && (parent.object === node || parent.computed);
|
||
case "Property": return mode !== "type" && (parent.value === node || parent.computed);
|
||
case "MethodDefinition":
|
||
case "PropertyDefinition":
|
||
case "AccessorProperty":
|
||
case "TSAbstractMethodDefinition":
|
||
case "TSAbstractPropertyDefinition":
|
||
case "TSAbstractAccessorProperty": return mode !== "type" && (parent.value === node || parent.computed);
|
||
case "ExportSpecifier": return mode !== "type" && parent.local === node;
|
||
case "ExportAllDeclaration": return false;
|
||
case "ImportSpecifier": return false;
|
||
case "LabeledStatement":
|
||
case "BreakStatement":
|
||
case "ContinueStatement": return false;
|
||
case "MetaProperty": return false;
|
||
case "ImportAttribute": return false;
|
||
case "TSEnumMember": return mode !== "type" && parent.id !== node;
|
||
case "TSMappedType": return false;
|
||
case "TSTypeReference": return mode !== "value";
|
||
case "TSQualifiedName": return mode !== "value" && parent.left === node;
|
||
case "TSClassImplements":
|
||
case "TSInterfaceHeritage": return mode !== "value" && parent.expression === node;
|
||
case "TSTypeParameter":
|
||
case "TSInterfaceDeclaration":
|
||
case "TSTypeAliasDeclaration":
|
||
case "TSPropertySignature":
|
||
case "TSMethodSignature":
|
||
case "TSIndexSignature": return false;
|
||
case "TSFunctionType":
|
||
case "TSConstructorType":
|
||
case "TSCallSignatureDeclaration":
|
||
case "TSConstructSignatureDeclaration": return false;
|
||
}
|
||
return mode !== "type";
|
||
}
|
||
function getUndeclaredIdentifiersInFunction(node) {
|
||
const scopeTracker = new ScopeTracker({ preserveExitedScopes: true });
|
||
const undeclaredIdentifiers = /* @__PURE__ */ new Set();
|
||
function isIdentifierUndeclared(node, parent) {
|
||
return isReferenceIdentifier(node, parent) && !scopeTracker.isDeclared(node.name);
|
||
}
|
||
walk(node, { scopeTracker });
|
||
scopeTracker.freeze();
|
||
walk(node, {
|
||
scopeTracker,
|
||
enter(node, parent) {
|
||
if ((node.type === "Identifier" || node.type === "JSXIdentifier") && isIdentifierUndeclared(node, parent)) undeclaredIdentifiers.add(node.name);
|
||
}
|
||
});
|
||
return Array.from(undeclaredIdentifiers);
|
||
}
|
||
/**
|
||
* A function to check whether scope A is a child of scope B.
|
||
* @example
|
||
* ```ts
|
||
* isChildScope('0-1-2', '0-1') // true
|
||
* isChildScope('0-1', '0-1') // false
|
||
* ```
|
||
*
|
||
* @param a the child scope
|
||
* @param b the parent scope
|
||
* @returns true if scope A is a child of scope B, false otherwise (also when they are the same)
|
||
*/
|
||
function isChildScope(a, b) {
|
||
return a.length > b.length && a.startsWith(b) && (b === "" || a[b.length] === "-");
|
||
}
|
||
var BaseNode = class {
|
||
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);
|
||
}
|
||
};
|
||
var ScopeTrackerIdentifier = class extends BaseNode {
|
||
type = "Identifier";
|
||
get start() {
|
||
return this.node.start;
|
||
}
|
||
get end() {
|
||
return this.node.end;
|
||
}
|
||
};
|
||
var ScopeTrackerFunctionParam = class 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;
|
||
}
|
||
};
|
||
var ScopeTrackerFunction = class extends BaseNode {
|
||
type = "Function";
|
||
get start() {
|
||
return this.node.start;
|
||
}
|
||
get end() {
|
||
return this.node.end;
|
||
}
|
||
};
|
||
var ScopeTrackerFunctionArguments = class extends BaseNode {
|
||
type = "FunctionArguments";
|
||
get start() {
|
||
return this.node.start;
|
||
}
|
||
get end() {
|
||
return this.node.end;
|
||
}
|
||
};
|
||
var ScopeTrackerVariable = class 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;
|
||
}
|
||
};
|
||
var ScopeTrackerImport = class 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;
|
||
}
|
||
};
|
||
var ScopeTrackerCatchParam = class 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;
|
||
}
|
||
};
|
||
//#endregion
|
||
export { ScopeTracker, ScopeTrackerCatchParam, ScopeTrackerFunction, ScopeTrackerFunctionArguments, ScopeTrackerFunctionParam, ScopeTrackerIdentifier, ScopeTrackerImport, ScopeTrackerVariable, getUndeclaredIdentifiersInFunction, isBindingIdentifier, isOnlyBindingIdentifier, isReferenceIdentifier, parseAndWalk, walk };
|