Skip to content

Commit

Permalink
feat(ast): add support for mutable node replacements (#4121)
Browse files Browse the repository at this point in the history
Refs #4120
  • Loading branch information
char0n committed May 20, 2024
1 parent 7e9c0a6 commit b37ecd2
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 166 deletions.
37 changes: 31 additions & 6 deletions packages/apidom-ast/src/traversal/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ export const visit = (
let inArray = Array.isArray(root);
let keys = [root];
let index = -1;
let parent;
let parent: any;
let edits = [];
let node = root;
const path: any[] = [];
Expand All @@ -376,7 +376,7 @@ export const visit = (
do {
index += 1;
const isLeaving = index === keys.length;
let key;
let key: any;
const isEdited = isLeaving && edits.length !== 0;
if (isLeaving) {
key = ancestors.length === 0 ? undefined : path.pop();
Expand Down Expand Up @@ -444,8 +444,21 @@ export const visit = (
for (const [stateKey, stateValue] of Object.entries(state)) {
visitor[stateKey] = stateValue;
}

const link = {
// eslint-disable-next-line @typescript-eslint/no-loop-func
replaceWith(newNode: any, replacer?: any) {
if (typeof replacer === 'function') {
replacer(newNode, node, key, parent, path, ancestors);
} else if (parent) {
parent[key] = newNode;
}
node = newNode;
},
};

// retrieve result
result = visitFn.call(visitor, node, key, parent, path, ancestors);
result = visitFn.call(visitor, node, key, parent, path, ancestors, link);
}

// check if the visitor is async
Expand Down Expand Up @@ -531,7 +544,7 @@ visit[Symbol.for('nodejs.util.promisify.custom')] = async (
let inArray = Array.isArray(root);
let keys = [root];
let index = -1;
let parent;
let parent: any;
let edits = [];
let node: any = root;
const path: any[] = [];
Expand All @@ -541,7 +554,7 @@ visit[Symbol.for('nodejs.util.promisify.custom')] = async (
do {
index += 1;
const isLeaving = index === keys.length;
let key;
let key: any;
const isEdited = isLeaving && edits.length !== 0;
if (isLeaving) {
key = ancestors.length === 0 ? undefined : path.pop();
Expand Down Expand Up @@ -610,8 +623,20 @@ visit[Symbol.for('nodejs.util.promisify.custom')] = async (
visitor[stateKey] = stateValue;
}

const link = {
// eslint-disable-next-line @typescript-eslint/no-loop-func
replaceWith(newNode: any, replacer?: any) {
if (typeof replacer === 'function') {
replacer(newNode, node, key, parent, path, ancestors);
} else if (parent) {
parent[key] = newNode;
}
node = newNode;
},
};

// retrieve result
result = await visitFn.call(visitor, node, key, parent, path, ancestors); // eslint-disable-line no-await-in-loop
result = await visitFn.call(visitor, node, key, parent, path, ancestors, link); // eslint-disable-line no-await-in-loop
}

if (result === breakSymbol) {
Expand Down
84 changes: 84 additions & 0 deletions packages/apidom-ast/test/traversal/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,53 @@ describe('visitor', function () {
});
});

context('given node is replaced with mutation', function () {
let visitor: any;
let structure: any;

beforeEach(function () {
visitor = {
number(node: any, key: any, parent: any, path: any, ancestors: any, link: any) {
if (node.type === 'number') {
link.replaceWith({ type: 'foo', value: 'bar' });
}
},
foo: {
leave: sinon.spy(),
},
};
structure = {
type: 'object',
children: [
{ type: 'number', value: 1 },
{ type: 'string', value: 'test' },
{ type: 'object', children: [] },
],
};
});

specify('should replace node', function () {
// @ts-ignore
visit(structure, visitor, { keyMap: { object: ['children'] } });

assert.deepEqual(structure, {
type: 'object',
children: [
{ type: 'foo', value: 'bar' },
{ type: 'string', value: 'test' },
{ type: 'object', children: [] },
],
});
});

specify('should revisit replaced node', function () {
// @ts-ignore
visit(structure, visitor, { keyMap: { object: ['children'] } });

assert.isTrue(visitor.foo.leave.calledOnce);
});
});

context('mergeAll', function () {
context('given exposeEdits=true', function () {
specify('should see edited node', function () {
Expand Down Expand Up @@ -168,6 +215,43 @@ describe('visitor', function () {
});
});

specify('should see replaced node by mutation in leave hook', function () {
const visitor1 = {
string: {
enter(node: any, key: any, parent: any, path: any, ancestors: any, link: any) {
link.replaceWith({ 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: [] },
],
});
});

context('given async visitor in sync mode', function () {
specify('should throw error', function () {
const visitor1 = {
Expand Down
4 changes: 2 additions & 2 deletions packages/apidom-core/test/refractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('refractor', function () {
},
);

assert.lengthOf(plugin1Spec.visitor.ObjectElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.ObjectElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -232,7 +232,7 @@ describe('refractor', function () {
},
);

assert.lengthOf(plugin2Spec.visitor.ObjectElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.ObjectElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down
4 changes: 2 additions & 2 deletions packages/apidom-ns-api-design-systems/test/refractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.lengthOf(plugin1Spec.visitor.MainElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.MainElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -241,7 +241,7 @@ describe('refractor', function () {
plugins: [plugin1, plugin2],
});

assert.lengthOf(plugin2Spec.visitor.MainElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.MainElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down
4 changes: 2 additions & 2 deletions packages/apidom-ns-asyncapi-2/test/refractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.lengthOf(plugin1Spec.visitor.AsyncApiVersionElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.AsyncApiVersionElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -321,7 +321,7 @@ describe('refractor', function () {
plugins: [plugin1, plugin2],
});

assert.lengthOf(plugin2Spec.visitor.AsyncApiVersionElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.AsyncApiVersionElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.lengthOf(plugin1Spec.visitor.MediaElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.MediaElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -266,7 +266,7 @@ describe('refractor', function () {
plugins: [plugin1, plugin2],
});

assert.lengthOf(plugin2Spec.visitor.MediaElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.MediaElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.lengthOf(plugin1Spec.visitor.MediaElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.MediaElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -266,7 +266,7 @@ describe('refractor', function () {
plugins: [plugin1, plugin2],
});

assert.lengthOf(plugin2Spec.visitor.MediaElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.MediaElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.lengthOf(plugin1Spec.visitor.LinkDescriptionElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.LinkDescriptionElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -270,7 +270,7 @@ describe('refractor', function () {
plugins: [plugin1, plugin2],
});

assert.lengthOf(plugin2Spec.visitor.LinkDescriptionElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.LinkDescriptionElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down
4 changes: 2 additions & 2 deletions packages/apidom-ns-openapi-2/test/refractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.lengthOf(plugin1Spec.visitor.SwaggerVersionElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.SwaggerVersionElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -243,7 +243,7 @@ describe('refractor', function () {
plugins: [plugin1, plugin2],
});

assert.lengthOf(plugin2Spec.visitor.SwaggerVersionElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.SwaggerVersionElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down
4 changes: 2 additions & 2 deletions packages/apidom-ns-openapi-3-0/test/refractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.lengthOf(plugin1Spec.visitor.OpenapiElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.OpenapiElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -259,7 +259,7 @@ describe('refractor', function () {
plugins: [plugin1, plugin2],
});

assert.lengthOf(plugin2Spec.visitor.OpenapiElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.OpenapiElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down
4 changes: 2 additions & 2 deletions packages/apidom-ns-openapi-3-1/test/refractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.lengthOf(plugin1Spec.visitor.OpenapiElement.firstCall.args, 5);
assert.lengthOf(plugin1Spec.visitor.OpenapiElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down Expand Up @@ -239,7 +239,7 @@ describe('refractor', function () {
plugins: [plugin1, plugin2],
});

assert.lengthOf(plugin2Spec.visitor.OpenapiElement.firstCall.args, 5);
assert.lengthOf(plugin2Spec.visitor.OpenapiElement.firstCall.args, 6);
});

specify('should receive node as first argument', function () {
Expand Down

0 comments on commit b37ecd2

Please sign in to comment.