271 lines
11 KiB
TypeScript
271 lines
11 KiB
TypeScript
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 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, {
|
||
* 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 };
|