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

271 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { SyncHandler } from 'estree-walker';
import { Node, Function, ArrowFunctionExpression, VariableDeclaration, ImportDeclarationSpecifier, CatchClause, IdentifierName, IdentifierReference, BindingIdentifier, LabelIdentifier, TSIndexSignatureName, Program, ParseResult, ParserOptions } from 'oxc-parser';
/**
* 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 JavaScripts 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, {
* scope: scopeTracker,
* enter(node) {
* // ...
* },
* })
* ```
*
* @see parseAndWalk
* @see walk
*/
declare class ScopeTracker {
protected scopeIndexStack: number[];
protected scopeIndexKey: string;
protected scopes: Map<string, Map<string, ScopeTrackerNode>>;
protected options: Partial<ScopeTrackerOptions>;
protected isFrozen: boolean;
constructor(options?: ScopeTrackerOptions);
protected updateScopeIndexKey(): void;
protected pushScope(): void;
protected popScope(): void;
protected declareIdentifier(name: string, data: ScopeTrackerNode): void;
protected declareFunctionParameter(param: Node, fn: Function | ArrowFunctionExpression): void;
protected declarePattern(pattern: Node, parent: VariableDeclaration | ArrowFunctionExpression | CatchClause | Function): void;
protected processNodeEnter(node: Node): void;
protected processNodeLeave(node: Node): void;
/**
* Check if an identifier is declared in the current scope or any parent scope.
* @param name the identifier name to check
*/
isDeclared(name: string): boolean;
/**
* Get the declaration node for a given identifier name.
* @param name the identifier name to look up
*/
getDeclaration(name: string): ScopeTrackerNode | null;
/**
* Get the current scope key.
*/
getCurrentScope(): string;
/**
* 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: string): boolean;
/**
* 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(): void;
}
declare function isBindingIdentifier(node: Node, parent: Node | null): boolean;
declare function getUndeclaredIdentifiersInFunction(node: Function | ArrowFunctionExpression): string[];
declare abstract class BaseNode<T extends Node = Node> {
abstract type: string;
readonly scope: string;
node: T;
constructor(node: T, scope: string);
/**
* The starting position of the entire node relevant for code transformation.
* For instance, for a reference to a variable (ScopeTrackerVariable -> Identifier), this would refer to the start of the VariableDeclaration.
*/
abstract get start(): number;
/**
* The ending position of the entire node relevant for code transformation.
* For instance, for a reference to a variable (ScopeTrackerVariable -> Identifier), this would refer to the end of the VariableDeclaration.
*/
abstract get end(): number;
/**
* Check if the node is defined under a specific scope.
* @param scope
*/
isUnderScope(scope: string): boolean;
}
declare class ScopeTrackerIdentifier extends BaseNode<Identifier> {
type: "Identifier";
get start(): number;
get end(): number;
}
declare class ScopeTrackerFunctionParam extends BaseNode {
type: "FunctionParam";
fnNode: Function | ArrowFunctionExpression;
constructor(node: Node, scope: string, fnNode: Function | ArrowFunctionExpression);
/**
* @deprecated The representation of this position may change in the future. Use `.fnNode.start` instead for now.
*/
get start(): number;
/**
* @deprecated The representation of this position may change in the future. Use `.fnNode.end` instead for now.
*/
get end(): number;
}
declare class ScopeTrackerFunction extends BaseNode<Function | ArrowFunctionExpression> {
type: "Function";
get start(): number;
get end(): number;
}
declare class ScopeTrackerVariable extends BaseNode<Identifier> {
type: "Variable";
variableNode: VariableDeclaration;
constructor(node: Identifier, scope: string, variableNode: VariableDeclaration);
get start(): number;
get end(): number;
}
declare class ScopeTrackerImport extends BaseNode<ImportDeclarationSpecifier> {
type: "Import";
importNode: Node;
constructor(node: ImportDeclarationSpecifier, scope: string, importNode: Node);
get start(): number;
get end(): number;
}
declare class ScopeTrackerCatchParam extends BaseNode {
type: "CatchParam";
catchNode: CatchClause;
constructor(node: Node, scope: string, catchNode: CatchClause);
get start(): number;
get end(): number;
}
type ScopeTrackerNode = ScopeTrackerFunctionParam | ScopeTrackerFunction | ScopeTrackerVariable | ScopeTrackerIdentifier | ScopeTrackerImport | ScopeTrackerCatchParam;
interface ScopeTrackerOptions {
/**
* If true, the scope tracker will preserve exited scopes in memory.
* This is necessary when you want to do a pre-pass to collect all identifiers before walking, for example.
* @default false
*/
preserveExitedScopes?: boolean;
}
type Identifier = IdentifierName | IdentifierReference | BindingIdentifier | LabelIdentifier | TSIndexSignatureName;
interface WalkerCallbackContext {
/**
* The key of the current node within its parent node object, if applicable.
*
* For instance, when processing a `VariableDeclarator` node, this would be the `declarations` key of the parent `VariableDeclaration` node.
* @example
* {
* type: 'VariableDeclaration',
* declarations: [[Object]],
* // ...
* },
* { // <-- when processing this, the key would be 'declarations'
* type: 'VariableDeclarator',
* // ...
* },
*/
key: string | number | symbol | null | undefined;
/**
* The zero-based index of the current node within its parent's children array, if applicable.
* For instance, when processing a `VariableDeclarator` node,
* this would be the index of the current `VariableDeclarator` node within the `declarations` array.
*
* This is `null` when the node is not part of an array and `undefined` for the root `Program` node.
*
* @example
* {
* type: 'VariableDeclaration',
* declarations: [[Object]],
* // ...
* },
* { // <-- when processing this, the index would be 0
* type: 'VariableDeclarator',
* // ...
* },
*/
index: number | null | undefined;
/**
* The full Abstract Syntax Tree (AST) that is being walked, starting from the root node.
*/
ast: Program | Node;
}
type WalkerCallback = (this: ThisParameterType<SyncHandler>, node: Node, parent: Node | null, ctx: WalkerCallbackContext) => void;
interface _WalkOptions {
/**
* The instance of `ScopeTracker` to use for tracking declarations and references.
* @see ScopeTracker
* @default undefined
*/
scopeTracker: ScopeTracker;
}
interface WalkOptions extends Partial<_WalkOptions> {
/**
* The function to be called when entering a node.
*/
enter: WalkerCallback;
/**
* The function to be called when leaving a node.
*/
leave: WalkerCallback;
}
/**
* 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.
*/
declare function walk(input: Program | Node, options: Partial<WalkOptions>): Program | Node | null;
interface ParseAndWalkOptions extends WalkOptions {
/**
* The options for `oxc-parser` to use when parsing the code.
*/
parseOptions: ParserOptions;
}
/**
* Parse the code and walk the AST with the given callback, which is called when entering a node.
* @param code The string with the code to parse and walk. This can be JavaScript, TypeScript, jsx, or tsx.
* @param sourceFilename The filename of the source code. This is used to determine the language of the code.
* @param callback The callback to be called when entering a node.
*/
declare function parseAndWalk(code: string, sourceFilename: string, callback: WalkerCallback): ParseResult;
/**
* Parse the code and walk the AST with the given callback(s).
* @param code The string with the code to parse and walk. This can be JavaScript, TypeScript, jsx, or tsx.
* @param sourceFilename The filename of the source code. This is used to determine the language of the code.
* @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.
*/
declare function parseAndWalk(code: string, sourceFilename: string, options: Partial<ParseAndWalkOptions>): ParseResult;
export { ScopeTracker, getUndeclaredIdentifiersInFunction, isBindingIdentifier, parseAndWalk, walk };
export type { Identifier, ScopeTrackerNode };