Skip to content

Commit

Permalink
feat(ast): see edits by the following merged visitors (#3412)
Browse files Browse the repository at this point in the history
This change is specific to  function
which allows visitors to run in parallel.
  • Loading branch information
char0n authored Nov 20, 2023
1 parent a8106de commit 6499557
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 63 deletions.
62 changes: 43 additions & 19 deletions packages/apidom-ast/src/traversal/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,48 +55,72 @@ export const cloneNode = (node: any) =>
* parallel. Each visitor will be visited for each node before moving on.
*
* If a prior visitor edits a node, no following visitors will see that node.
* `exposeEdits=true` can be used to exoise the edited node from the previous visitors.
*/
export const mergeAll = (
visitors: any[],
{ visitFnGetter = getVisitFn, nodeTypeGetter = getNodeType } = {},
{
visitFnGetter = getVisitFn,
nodeTypeGetter = getNodeType,
breakSymbol = BREAK,
deleteNodeSymbol = null,
skipVisitingNodeSymbol = false,
exposeEdits = false,
} = {},
) => {
const skipping = new Array(visitors.length).fill(null);
const skipSymbol = Symbol('skip');
const skipping = new Array(visitors.length).fill(skipSymbol);

return {
enter(node: any, ...rest: any[]) {
let currentNode = node;
let hasChanged = false;

for (let i = 0; i < visitors.length; i += 1) {
if (skipping[i] === null) {
const fn = visitFnGetter(visitors[i], nodeTypeGetter(node), /* isLeaving */ false);
if (typeof fn === 'function') {
const result = fn.call(visitors[i], node, ...rest);
if (result === false) {
if (skipping[i] === skipSymbol) {
const visitFn = visitFnGetter(visitors[i], nodeTypeGetter(currentNode), false);

if (typeof visitFn === 'function') {
const result: any = visitFn.call(visitors[i], currentNode, ...rest);

if (result === skipVisitingNodeSymbol) {
skipping[i] = node;
} else if (result === BREAK) {
skipping[i] = BREAK;
} else if (result !== undefined) {
} else if (result === breakSymbol) {
skipping[i] = breakSymbol;
} else if (result === deleteNodeSymbol) {
return result;
} else if (result !== undefined) {
if (exposeEdits) {
currentNode = result;
hasChanged = true;
} else {
return result;
}
}
}
}
}
return undefined;

return hasChanged ? currentNode : undefined;
},
leave(node: any, ...rest: any[]) {
for (let i = 0; i < visitors.length; i += 1) {
if (skipping[i] === null) {
const fn = visitFnGetter(visitors[i], nodeTypeGetter(node), /* isLeaving */ true);
if (typeof fn === 'function') {
const result = fn.call(visitors[i], node, ...rest);
if (result === BREAK) {
skipping[i] = BREAK;
} else if (result !== undefined && result !== false) {
if (skipping[i] === skipSymbol) {
const visitFn = visitFnGetter(visitors[i], nodeTypeGetter(node), true);

if (typeof visitFn === 'function') {
const result = visitFn.call(visitors[i], node, ...rest);
if (result === breakSymbol) {
skipping[i] = breakSymbol;
} else if (result !== undefined && result !== skipVisitingNodeSymbol) {
return result;
}
}
} else if (skipping[i] === node) {
skipping[i] = null;
skipping[i] = skipSymbol;
}
}

return undefined;
},
};
Expand Down
148 changes: 148 additions & 0 deletions packages/apidom-ast/test/traversal/visitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import sinon from 'sinon';
import { assert } from 'chai';

import { visit, mergeAllVisitors } from '../../src';

describe('visitor', function () {
context('given structure with cycle', function () {
specify('should skip over a sub-tree to avoid recursion', function () {
const visitor = {
enter() {},
};
const structure = {
type: 'object',
children: [
{ type: 'number', value: 1 },
{ type: 'string', value: 'test' },
{ type: 'object', children: [] },
],
};
// @ts-ignore
structure.children[2].children.push(structure);

sinon.spy(visitor, 'enter');
// @ts-ignore
visit(structure, visitor, { keyMap: { object: ['children'] } });

// @ts-ignore
assert.strictEqual(visitor.enter.callCount, 4);
});
});

context('mergeAll', function () {
context('given exposeEdits=true', function () {
specify('should see edited node', function () {
const visitor1 = {
string: {
enter() {
return { type: 'boolean', value: true };
},
},
};
const visitor2 = {
boolean: {
enter(node: any) {
node.value = false; // eslint-disable-line no-param-reassign
},
},
};
const structure = {
type: 'object',
children: [
{ type: 'number', value: 1 },
{ type: 'string', value: 'test' },
{ type: 'object', children: [] },
],
};
const mergedVisitor = mergeAllVisitors([visitor1, visitor2], { exposeEdits: true });
// @ts-ignore
const newStructure = visit(structure, mergedVisitor, { keyMap: { object: ['children'] } });

assert.deepEqual(newStructure, {
type: 'object',
children: [
{ type: 'number', value: 1 },
{ type: 'boolean', value: false },
{ type: 'object', children: [] },
],
});
});
});

context('given exposeEditor=false', function () {
specify('should not see edited node', function () {
const visitor1 = {
string: {
enter() {
return { type: 'boolean', value: true };
},
},
};
const visitor2 = {
boolean: {
enter(node: any) {
node.value = false; // eslint-disable-line no-param-reassign
},
},
};
const structure = {
type: 'object',
children: [
{ type: 'number', value: 1 },
{ type: 'string', value: 'test' },
{ type: 'object', children: [] },
],
};
const mergedVisitor = mergeAllVisitors([visitor1, visitor2]);
// @ts-ignore
const newStructure = visit(structure, mergedVisitor, { keyMap: { object: ['children'] } });

assert.deepEqual(newStructure, {
type: 'object',
children: [
{ type: 'number', value: 1 },
{ type: 'boolean', value: true },
{ type: 'object', children: [] },
],
});
});
});

specify('should see edited node in leave hook', function () {
const visitor1 = {
string: {
enter() {
return { type: 'foo', value: 'bar' };
},
},
};
const visitor2 = {
foo: {
leave(node: any) {
node.value = 'foo'; // eslint-disable-line no-param-reassign
},
},
};
const structure = {
type: 'object',
children: [
{ type: 'number', value: 1 },
{ type: 'string', value: 2 },
{ type: 'object', children: [] },
],
};
const mergedVisitor = mergeAllVisitors([visitor1, visitor2]);
// @ts-ignore
const newStructure = visit(structure, mergedVisitor, { keyMap: { object: ['children'] } });

assert.deepEqual(newStructure, {
type: 'object',
children: [
{ type: 'number', value: 1 },
{ type: 'foo', value: 'foo' },
{ type: 'object', children: [] },
],
});
});
});
});
31 changes: 0 additions & 31 deletions packages/apidom-ast/test/visitor.ts

This file was deleted.

33 changes: 20 additions & 13 deletions packages/apidom-core/src/refractor/plugins/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import { Element } from 'minim';
import { propOr } from 'ramda';
import { mergeDeepRight, propOr } from 'ramda';
import { invokeArgs } from 'ramda-adjunct';

import createToolbox from '../../toolbox';
import { getNodeType, mergeAllVisitors, visit } from '../../../traversal/visitor';

const defaultDispatchPluginsOptions = {
toolboxCreator: createToolbox,
visitorOptions: {
nodeTypeGetter: getNodeType,
exposeEdits: true,
},
};

// eslint-disable-next-line import/prefer-default-export
export const dispatchPlugins = <T extends Element>(element: T, plugins: any[], options = {}): T => {
export const dispatchPlugins = <T extends Element>(
element: T,
plugins: ((toolbox: any) => object)[],
options = {},
): T => {
if (plugins.length === 0) return element;

const toolboxCreator = propOr(createToolbox, 'toolboxCreator', options) as typeof createToolbox;
const visitorOptions = propOr({}, 'visitorOptions', options);
const nodeTypeGetter = propOr(
getNodeType,
'nodeTypeGetter',
visitorOptions,
) as typeof getNodeType;
const mergedOptions = mergeDeepRight(defaultDispatchPluginsOptions, options);
const { toolboxCreator, visitorOptions } = mergedOptions;
const toolbox = toolboxCreator();
const pluginsSpecs = plugins.map((plugin: any) => plugin(toolbox));
const pluginsVisitor = mergeAllVisitors(pluginsSpecs.map(propOr({}, 'visitor')), {
nodeTypeGetter,
const pluginsSpecs = plugins.map((plugin) => plugin(toolbox));
const mergedPluginsVisitor = mergeAllVisitors(pluginsSpecs.map(propOr({}, 'visitor')), {
...visitorOptions,
});

pluginsSpecs.forEach(invokeArgs(['pre'], []));
const newElement = visit(element, pluginsVisitor, visitorOptions as any);
const newElement = visit(element, mergedPluginsVisitor, visitorOptions as any);
pluginsSpecs.forEach(invokeArgs(['post'], []));
return newElement as T;
};

0 comments on commit 6499557

Please sign in to comment.