You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
333 lines
7.0 KiB
333 lines
7.0 KiB
// @ts-check |
|
/** @typedef { import('estree').BaseNode} BaseNode */ |
|
|
|
/** @typedef {{ |
|
skip: () => void; |
|
remove: () => void; |
|
replace: (node: BaseNode) => void; |
|
}} WalkerContext */ |
|
|
|
class WalkerBase { |
|
constructor() { |
|
/** @type {boolean} */ |
|
this.should_skip = false; |
|
|
|
/** @type {boolean} */ |
|
this.should_remove = false; |
|
|
|
/** @type {BaseNode | null} */ |
|
this.replacement = null; |
|
|
|
/** @type {WalkerContext} */ |
|
this.context = { |
|
skip: () => (this.should_skip = true), |
|
remove: () => (this.should_remove = true), |
|
replace: (node) => (this.replacement = node) |
|
}; |
|
} |
|
|
|
/** |
|
* |
|
* @param {any} parent |
|
* @param {string} prop |
|
* @param {number} index |
|
* @param {BaseNode} node |
|
*/ |
|
replace(parent, prop, index, node) { |
|
if (parent) { |
|
if (index !== null) { |
|
parent[prop][index] = node; |
|
} else { |
|
parent[prop] = node; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* |
|
* @param {any} parent |
|
* @param {string} prop |
|
* @param {number} index |
|
*/ |
|
remove(parent, prop, index) { |
|
if (parent) { |
|
if (index !== null) { |
|
parent[prop].splice(index, 1); |
|
} else { |
|
delete parent[prop]; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// @ts-check |
|
|
|
/** @typedef { import('estree').BaseNode} BaseNode */ |
|
/** @typedef { import('./walker.js').WalkerContext} WalkerContext */ |
|
|
|
/** @typedef {( |
|
* this: WalkerContext, |
|
* node: BaseNode, |
|
* parent: BaseNode, |
|
* key: string, |
|
* index: number |
|
* ) => void} SyncHandler */ |
|
|
|
class SyncWalker extends WalkerBase { |
|
/** |
|
* |
|
* @param {SyncHandler} enter |
|
* @param {SyncHandler} leave |
|
*/ |
|
constructor(enter, leave) { |
|
super(); |
|
|
|
/** @type {SyncHandler} */ |
|
this.enter = enter; |
|
|
|
/** @type {SyncHandler} */ |
|
this.leave = leave; |
|
} |
|
|
|
/** |
|
* |
|
* @param {BaseNode} node |
|
* @param {BaseNode} parent |
|
* @param {string} [prop] |
|
* @param {number} [index] |
|
* @returns {BaseNode} |
|
*/ |
|
visit(node, parent, prop, index) { |
|
if (node) { |
|
if (this.enter) { |
|
const _should_skip = this.should_skip; |
|
const _should_remove = this.should_remove; |
|
const _replacement = this.replacement; |
|
this.should_skip = false; |
|
this.should_remove = false; |
|
this.replacement = null; |
|
|
|
this.enter.call(this.context, node, parent, prop, index); |
|
|
|
if (this.replacement) { |
|
node = this.replacement; |
|
this.replace(parent, prop, index, node); |
|
} |
|
|
|
if (this.should_remove) { |
|
this.remove(parent, prop, index); |
|
} |
|
|
|
const skipped = this.should_skip; |
|
const removed = this.should_remove; |
|
|
|
this.should_skip = _should_skip; |
|
this.should_remove = _should_remove; |
|
this.replacement = _replacement; |
|
|
|
if (skipped) return node; |
|
if (removed) return null; |
|
} |
|
|
|
for (const key in node) { |
|
const value = node[key]; |
|
|
|
if (typeof value !== "object") { |
|
continue; |
|
} else if (Array.isArray(value)) { |
|
for (let i = 0; i < value.length; i += 1) { |
|
if (value[i] !== null && typeof value[i].type === 'string') { |
|
if (!this.visit(value[i], node, key, i)) { |
|
// removed |
|
i--; |
|
} |
|
} |
|
} |
|
} else if (value !== null && typeof value.type === "string") { |
|
this.visit(value, node, key, null); |
|
} |
|
} |
|
|
|
if (this.leave) { |
|
const _replacement = this.replacement; |
|
const _should_remove = this.should_remove; |
|
this.replacement = null; |
|
this.should_remove = false; |
|
|
|
this.leave.call(this.context, node, parent, prop, index); |
|
|
|
if (this.replacement) { |
|
node = this.replacement; |
|
this.replace(parent, prop, index, node); |
|
} |
|
|
|
if (this.should_remove) { |
|
this.remove(parent, prop, index); |
|
} |
|
|
|
const removed = this.should_remove; |
|
|
|
this.replacement = _replacement; |
|
this.should_remove = _should_remove; |
|
|
|
if (removed) return null; |
|
} |
|
} |
|
|
|
return node; |
|
} |
|
} |
|
|
|
// @ts-check |
|
|
|
/** @typedef { import('estree').BaseNode} BaseNode */ |
|
/** @typedef { import('./walker').WalkerContext} WalkerContext */ |
|
|
|
/** @typedef {( |
|
* this: WalkerContext, |
|
* node: BaseNode, |
|
* parent: BaseNode, |
|
* key: string, |
|
* index: number |
|
* ) => Promise<void>} AsyncHandler */ |
|
|
|
class AsyncWalker extends WalkerBase { |
|
/** |
|
* |
|
* @param {AsyncHandler} enter |
|
* @param {AsyncHandler} leave |
|
*/ |
|
constructor(enter, leave) { |
|
super(); |
|
|
|
/** @type {AsyncHandler} */ |
|
this.enter = enter; |
|
|
|
/** @type {AsyncHandler} */ |
|
this.leave = leave; |
|
} |
|
|
|
/** |
|
* |
|
* @param {BaseNode} node |
|
* @param {BaseNode} parent |
|
* @param {string} [prop] |
|
* @param {number} [index] |
|
* @returns {Promise<BaseNode>} |
|
*/ |
|
async visit(node, parent, prop, index) { |
|
if (node) { |
|
if (this.enter) { |
|
const _should_skip = this.should_skip; |
|
const _should_remove = this.should_remove; |
|
const _replacement = this.replacement; |
|
this.should_skip = false; |
|
this.should_remove = false; |
|
this.replacement = null; |
|
|
|
await this.enter.call(this.context, node, parent, prop, index); |
|
|
|
if (this.replacement) { |
|
node = this.replacement; |
|
this.replace(parent, prop, index, node); |
|
} |
|
|
|
if (this.should_remove) { |
|
this.remove(parent, prop, index); |
|
} |
|
|
|
const skipped = this.should_skip; |
|
const removed = this.should_remove; |
|
|
|
this.should_skip = _should_skip; |
|
this.should_remove = _should_remove; |
|
this.replacement = _replacement; |
|
|
|
if (skipped) return node; |
|
if (removed) return null; |
|
} |
|
|
|
for (const key in node) { |
|
const value = node[key]; |
|
|
|
if (typeof value !== "object") { |
|
continue; |
|
} else if (Array.isArray(value)) { |
|
for (let i = 0; i < value.length; i += 1) { |
|
if (value[i] !== null && typeof value[i].type === 'string') { |
|
if (!(await this.visit(value[i], node, key, i))) { |
|
// removed |
|
i--; |
|
} |
|
} |
|
} |
|
} else if (value !== null && typeof value.type === "string") { |
|
await this.visit(value, node, key, null); |
|
} |
|
} |
|
|
|
if (this.leave) { |
|
const _replacement = this.replacement; |
|
const _should_remove = this.should_remove; |
|
this.replacement = null; |
|
this.should_remove = false; |
|
|
|
await this.leave.call(this.context, node, parent, prop, index); |
|
|
|
if (this.replacement) { |
|
node = this.replacement; |
|
this.replace(parent, prop, index, node); |
|
} |
|
|
|
if (this.should_remove) { |
|
this.remove(parent, prop, index); |
|
} |
|
|
|
const removed = this.should_remove; |
|
|
|
this.replacement = _replacement; |
|
this.should_remove = _should_remove; |
|
|
|
if (removed) return null; |
|
} |
|
} |
|
|
|
return node; |
|
} |
|
} |
|
|
|
// @ts-check |
|
|
|
/** @typedef { import('estree').BaseNode} BaseNode */ |
|
/** @typedef { import('./sync.js').SyncHandler} SyncHandler */ |
|
/** @typedef { import('./async.js').AsyncHandler} AsyncHandler */ |
|
|
|
/** |
|
* |
|
* @param {BaseNode} ast |
|
* @param {{ |
|
* enter?: SyncHandler |
|
* leave?: SyncHandler |
|
* }} walker |
|
* @returns {BaseNode} |
|
*/ |
|
function walk(ast, { enter, leave }) { |
|
const instance = new SyncWalker(enter, leave); |
|
return instance.visit(ast, null); |
|
} |
|
|
|
/** |
|
* |
|
* @param {BaseNode} ast |
|
* @param {{ |
|
* enter?: AsyncHandler |
|
* leave?: AsyncHandler |
|
* }} walker |
|
* @returns {Promise<BaseNode>} |
|
*/ |
|
async function asyncWalk(ast, { enter, leave }) { |
|
const instance = new AsyncWalker(enter, leave); |
|
return await instance.visit(ast, null); |
|
} |
|
|
|
export { asyncWalk, walk };
|
|
|