oxc-walker
A strongly-typed ESTree AST walker built on top of oxc-parser.
Usage
Install package:
# npm
npm install oxc-walker
# pnpm
pnpm install oxc-walker
Walk a parsed AST
import { parseSync } from 'oxc-parser'
import { walk } from 'oxc-walker'
const ast = parseSync('example.js', 'const x = 1')
walk(ast.program, {
enter(node, parent, ctx) {
// ...
},
})
Parse and walk directly
import { parseAndWalk } from 'oxc-walker'
parseAndWalk('const x = 1', 'example.js', (node, parent, ctx) => {
// ...
})
⚙️ API
walk(ast, options)
Walk an AST.
// options
interface WalkOptions {
/**
* The function to be called when entering a node.
*/
enter?: (node: Node, parent: Node | null, ctx: CallbackContext) => void
/**
* The function to be called when leaving a node.
*/
leave?: (node: Node, parent: Node | null, ctx: CallbackContext) => void
/**
* The instance of `ScopeTracker` to use for tracking declarations and references.
*/
scopeTracker?: ScopeTracker
}
interface CallbackContext {
/**
* The key of the current node within its parent node object, if applicable.
*/
key: string | number | symbol | null | undefined
/**
* The zero-based index of the current node within its parent's children array, if applicable.
*/
index: number | null
/**
* The full Abstract Syntax Tree (AST) that is being walked, starting from the root node.
*/
ast: Program | Node
}
this.skip()
When called inside an enter
callback, prevents the node's children from being walked.
It is not available in leave
.
this.replace(newNode)
Replaces the current node with newNode
. When called inside enter
, the new node's children will be walked.
The leave callback will still be called with the original node.
⚠️ When a
ScopeTracker
is provided, callingthis.replace()
will not update its declarations.
this.remove()
Removes the current node from its parent. When called inside enter
, the removed node's children
will not be walked.
This has a higher precedence than this.replace()
, so if both are called, the node will be removed.
⚠️ When a
ScopeTracker
is provided, callingthis.remove()
will not update its declarations.
parseAndWalk(source, filename, callback, options?)
Parse the source code using oxc-parser
, walk the resulting AST and return the ParseResult
.
Overloads:
parseAndWalk(code, filename, enter)
parseAndWalk(code, filename, options)
interface ParseAndWalkOptions {
/**
* The function to be called when entering a node.
*/
enter?: (node: Node, parent: Node | null, ctx: CallbackContext) => void
/**
* The function to be called when leaving a node.
*/
leave?: (node: Node, parent: Node | null, ctx: CallbackContext) => void
/**
* The instance of `ScopeTracker` to use for tracking declarations and references.
*/
scopeTracker?: ScopeTracker
/**
* The options for `oxc-parser` to use when parsing the code.
*/
parseOptions?: ParserOptions
}
ScopeTracker
A utility to track scopes and declarations while walking an AST. It is designed to be used with the walk
function from this library.
interface ScopeTrackerOptions {
/**
* If true, the scope tracker will preserve exited scopes in memory.
* @default false
*/
preserveExitedScopes?: boolean
}
Example usage:
import { parseAndWalk, ScopeTracker } from 'oxc-walker'
const scopeTracker = new ScopeTracker()
parseAndWalk('const x = 1; function foo() { console.log(x) }', 'example.js', {
scopeTracker,
enter(node, parent) {
if (node.type === 'Identifier' && node.name === 'x' && parent?.type === 'CallExpression') {
const declaration = scopeTracker.getDeclaration(node.name)
console.log(declaration) // ScopeTrackerVariable
}
},
})
import { parseAndWalk, ScopeTracker, walk } from 'oxc-walker'
const code = `
function foo() {
console.log(a)
}
const a = 1
`
const scopeTracker = new ScopeTracker({
preserveExitedScopes: true,
})
// pre-pass to collect hoisted declarations
const { program } = parseAndWalk(code, 'example.js', {
scopeTracker,
})
// freeze the scope tracker to prevent further modifications
// and prepare it for second pass
scopeTracker.freeze()
// main pass to analyze references
walk(program, {
scopeTracker,
enter(node) {
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression' /* ... */) {
const declaration = scopeTracker.getDeclaration('a')
console.log(declaration) // ScopeTrackerVariable; would be `null` without the pre-pass
}
}
})
Helpers:
scopeTracker.isDeclared(name: string): boolean
- check if an identifier is declared in reference to the current scopescopeTracker.getDeclaration(name: string): ScopeTrackerNode | null
- get the scope tracker node with metadata for a given identifier name in reference to the current scopescopeTracker.freeze()
- freeze the scope tracker to prevent further modifications and prepare for second pass (useful for multi-pass analysis)scopeTracker.getCurrentScope(): string
- get the key of the current scope (a unique identifier for the scope, do not rely on its format)scopeTracker.isCurrentScopeUnder(scopeKey: string): boolean
- check if the current scope is a child of the given scope key
💻 Development
- Clone this repository
- Enable Corepack using
corepack enable
- Install dependencies using
pnpm install
- Run interactive tests using
pnpm dev
License
Made with ❤️
Published under MIT License.