diff --git a/packages/apidom-ast/src/traversal/visitor.ts b/packages/apidom-ast/src/traversal/visitor.ts index e79cdf9166..fc9aee74d6 100644 --- a/packages/apidom-ast/src/traversal/visitor.ts +++ b/packages/apidom-ast/src/traversal/visitor.ts @@ -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[] = []; @@ -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(); @@ -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 @@ -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[] = []; @@ -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(); @@ -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) { diff --git a/packages/apidom-ast/test/traversal/visitor.ts b/packages/apidom-ast/test/traversal/visitor.ts index 783ad72fe7..3ee58ee9b5 100644 --- a/packages/apidom-ast/test/traversal/visitor.ts +++ b/packages/apidom-ast/test/traversal/visitor.ts @@ -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 () { @@ -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 = { diff --git a/packages/apidom-core/test/refractor/index.ts b/packages/apidom-core/test/refractor/index.ts index 73a45e30a3..db26c8ba90 100644 --- a/packages/apidom-core/test/refractor/index.ts +++ b/packages/apidom-core/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-ns-api-design-systems/test/refractor/index.ts b/packages/apidom-ns-api-design-systems/test/refractor/index.ts index 1af8bd027b..5e1ca66afb 100644 --- a/packages/apidom-ns-api-design-systems/test/refractor/index.ts +++ b/packages/apidom-ns-api-design-systems/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-ns-asyncapi-2/test/refractor/index.ts b/packages/apidom-ns-asyncapi-2/test/refractor/index.ts index 1d9f7d133c..adbd77eefe 100644 --- a/packages/apidom-ns-asyncapi-2/test/refractor/index.ts +++ b/packages/apidom-ns-asyncapi-2/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-ns-json-schema-draft-4/test/refractor/index.ts b/packages/apidom-ns-json-schema-draft-4/test/refractor/index.ts index 4cfbed59fe..06bae260c1 100644 --- a/packages/apidom-ns-json-schema-draft-4/test/refractor/index.ts +++ b/packages/apidom-ns-json-schema-draft-4/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-ns-json-schema-draft-6/test/refractor/index.ts b/packages/apidom-ns-json-schema-draft-6/test/refractor/index.ts index b039085d30..56d6b5f42a 100644 --- a/packages/apidom-ns-json-schema-draft-6/test/refractor/index.ts +++ b/packages/apidom-ns-json-schema-draft-6/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-ns-json-schema-draft-7/test/refractor/index.ts b/packages/apidom-ns-json-schema-draft-7/test/refractor/index.ts index 544dece76d..9a28c2913e 100644 --- a/packages/apidom-ns-json-schema-draft-7/test/refractor/index.ts +++ b/packages/apidom-ns-json-schema-draft-7/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-ns-openapi-2/test/refractor/index.ts b/packages/apidom-ns-openapi-2/test/refractor/index.ts index 6735b12d2b..2547480e88 100644 --- a/packages/apidom-ns-openapi-2/test/refractor/index.ts +++ b/packages/apidom-ns-openapi-2/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-ns-openapi-3-0/test/refractor/index.ts b/packages/apidom-ns-openapi-3-0/test/refractor/index.ts index deaee454dd..b605fdc3ad 100644 --- a/packages/apidom-ns-openapi-3-0/test/refractor/index.ts +++ b/packages/apidom-ns-openapi-3-0/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-ns-openapi-3-1/test/refractor/index.ts b/packages/apidom-ns-openapi-3-1/test/refractor/index.ts index ba41ffca40..2347060ba5 100644 --- a/packages/apidom-ns-openapi-3-1/test/refractor/index.ts +++ b/packages/apidom-ns-openapi-3-1/test/refractor/index.ts @@ -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 () { @@ -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 () { diff --git a/packages/apidom-reference/src/dereference/strategies/asyncapi-2/visitor.ts b/packages/apidom-reference/src/dereference/strategies/asyncapi-2/visitor.ts index dca4210454..4ff7c4fd50 100644 --- a/packages/apidom-reference/src/dereference/strategies/asyncapi-2/visitor.ts +++ b/packages/apidom-reference/src/dereference/strategies/asyncapi-2/visitor.ts @@ -42,6 +42,20 @@ const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; // initialize element identity manager const identityManager = new IdentityManager(); +// custom mutation replacer +const mutationReplacer = ( + newElement: Element, + oldElement: Element, + key: string | number, + parent: Element | undefined, +) => { + if (isMemberElement(parent)) { + parent.value = newElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = newElement; // eslint-disable-line no-param-reassign + } +}; + export interface AsyncAPI2DereferenceVisitorOptions { readonly namespace: Namespace; readonly reference: Reference; @@ -145,6 +159,7 @@ class AsyncAPI2DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // skip current referencing element as it's already been access if (this.indirections.includes(referencingElement)) { @@ -230,11 +245,7 @@ class AsyncAPI2DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : false; } @@ -297,11 +308,7 @@ class AsyncAPI2DereferenceVisitor { cloneDeep(identityManager.identify(referencingElement)), ); - if (isMemberElement(parent)) { - parent.value = booleanJsonSchemaElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = booleanJsonSchemaElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(booleanJsonSchemaElement, mutationReplacer); return !parent ? booleanJsonSchemaElement : false; } @@ -327,11 +334,7 @@ class AsyncAPI2DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = mergedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = mergedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(mergedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -345,6 +348,7 @@ class AsyncAPI2DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // ignore ChannelItemElement without $ref field if (!isStringElement(referencingElement.$ref)) { @@ -427,11 +431,7 @@ class AsyncAPI2DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : undefined; } @@ -513,11 +513,7 @@ class AsyncAPI2DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = referencedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = referencedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(referencedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. diff --git a/packages/apidom-reference/src/dereference/strategies/openapi-2/visitor.ts b/packages/apidom-reference/src/dereference/strategies/openapi-2/visitor.ts index 5a18676679..0e22f13c16 100644 --- a/packages/apidom-reference/src/dereference/strategies/openapi-2/visitor.ts +++ b/packages/apidom-reference/src/dereference/strategies/openapi-2/visitor.ts @@ -44,6 +44,20 @@ const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; // initialize element identity manager const identityManager = new IdentityManager(); +// custom mutation replacer +const mutationReplacer = ( + newElement: Element, + oldElement: Element, + key: string | number, + parent: Element | undefined, +) => { + if (isMemberElement(parent)) { + parent.value = newElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = newElement; // eslint-disable-line no-param-reassign + } +}; + export interface OpenAPI2DereferenceVisitorOptions { readonly namespace: Namespace; readonly reference: Reference; @@ -147,6 +161,7 @@ class OpenAPI2DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // skip current referencing element as it's already been access if (this.indirections.includes(referencingElement)) { @@ -232,11 +247,7 @@ class OpenAPI2DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : false; } @@ -304,11 +315,7 @@ class OpenAPI2DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = mergedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = mergedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(mergedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -322,6 +329,7 @@ class OpenAPI2DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // ignore PathItemElement without $ref field if (!isStringElement(referencingElement.$ref)) { @@ -404,11 +412,7 @@ class OpenAPI2DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : false; } @@ -488,11 +492,7 @@ class OpenAPI2DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = referencedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = referencedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(referencedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -506,6 +506,7 @@ class OpenAPI2DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // skip current referencing element as it's already been access if (this.indirections.includes(referencingElement)) { @@ -576,6 +577,8 @@ class OpenAPI2DereferenceVisitor { // detect second deep dive into the same fragment and avoid it if (ancestorsLineage.includes(referencedElement)) { + reference.refSet!.circular = true; + if (this.options.dereference.circular === 'error') { throw new ApiDOMError('Circular reference detected'); } else if (this.options.dereference.circular === 'replace') { @@ -589,13 +592,7 @@ class OpenAPI2DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } - - reference.refSet!.circular = true; + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : false; } @@ -663,11 +660,7 @@ class OpenAPI2DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = mergedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = mergedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(mergedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. diff --git a/packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts b/packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts index 24cb4444e3..96fb0aa7dc 100644 --- a/packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts +++ b/packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts @@ -46,6 +46,20 @@ const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; // initialize element identity manager const identityManager = new IdentityManager(); +// custom mutation replacer +const mutationReplacer = ( + newElement: Element, + oldElement: Element, + key: string | number, + parent: Element | undefined, +) => { + if (isMemberElement(parent)) { + parent.value = newElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = newElement; // eslint-disable-line no-param-reassign + } +}; + export interface OpenAPI3_0DereferenceVisitorOptions { readonly namespace: Namespace; readonly reference: Reference; @@ -149,6 +163,7 @@ class OpenAPI3_0DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // skip current referencing element as it's already been access if (this.indirections.includes(referencingElement)) { @@ -234,11 +249,7 @@ class OpenAPI3_0DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : false; } @@ -305,11 +316,7 @@ class OpenAPI3_0DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = mergedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = mergedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(mergedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -323,6 +330,7 @@ class OpenAPI3_0DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // ignore PathItemElement without $ref field if (!isStringElement(referencingElement.$ref)) { @@ -405,11 +413,7 @@ class OpenAPI3_0DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : undefined; } @@ -491,11 +495,7 @@ class OpenAPI3_0DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = referencedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = referencedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(referencedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -507,6 +507,9 @@ class OpenAPI3_0DereferenceVisitor { linkElement: LinkElement, key: string | number, parent: Element | undefined, + path: (string | number)[], + ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // ignore LinkElement without operationRef or operationId field if (!isStringElement(linkElement.operationRef) && !isStringElement(linkElement.operationId)) { @@ -565,11 +568,7 @@ class OpenAPI3_0DereferenceVisitor { /** * Transclude Link Object containing Operation Object in its meta. */ - if (isMemberElement(parent)) { - parent.value = linkElementCopy; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = linkElementCopy; // eslint-disable-line no-param-reassign - } + link.replaceWith(linkElementCopy, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -596,11 +595,7 @@ class OpenAPI3_0DereferenceVisitor { /** * Transclude Link Object containing Operation Object in its meta. */ - if (isMemberElement(parent)) { - parent.value = linkElementCopy; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = linkElementCopy; // eslint-disable-line no-param-reassign - } + link.replaceWith(linkElementCopy, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -615,6 +610,9 @@ class OpenAPI3_0DereferenceVisitor { exampleElement: ExampleElement, key: string | number, parent: Element | undefined, + path: (string | number)[], + ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // ignore ExampleElement without externalValue field if (!isStringElement(exampleElement.externalValue)) { @@ -656,11 +654,7 @@ class OpenAPI3_0DereferenceVisitor { /** * Transclude Example Object containing external value. */ - if (isMemberElement(parent)) { - parent.value = exampleElementCopy; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = exampleElementCopy; // eslint-disable-line no-param-reassign - } + link.replaceWith(exampleElementCopy, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. diff --git a/packages/apidom-reference/src/dereference/strategies/openapi-3-1/visitor.ts b/packages/apidom-reference/src/dereference/strategies/openapi-3-1/visitor.ts index c885394b36..92ba3ea020 100644 --- a/packages/apidom-reference/src/dereference/strategies/openapi-3-1/visitor.ts +++ b/packages/apidom-reference/src/dereference/strategies/openapi-3-1/visitor.ts @@ -57,6 +57,20 @@ const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; // initialize element identity manager const identityManager = new IdentityManager(); +// custom mutation replacer +const mutationReplacer = ( + newElement: Element, + oldElement: Element, + key: string | number, + parent: Element | undefined, +) => { + if (isMemberElement(parent)) { + parent.value = newElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = newElement; // eslint-disable-line no-param-reassign + } +}; + export interface OpenAPI3_1DereferenceVisitorOptions { readonly namespace: Namespace; readonly reference: Reference; @@ -160,6 +174,7 @@ class OpenAPI3_1DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // skip current referencing element as it's already been access if (this.indirections.includes(referencingElement)) { @@ -243,11 +258,7 @@ class OpenAPI3_1DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : false; } @@ -330,11 +341,7 @@ class OpenAPI3_1DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = mergedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = mergedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(mergedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -348,6 +355,7 @@ class OpenAPI3_1DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // ignore PathItemElement without $ref field if (!isStringElement(referencingElement.$ref)) { @@ -430,11 +438,7 @@ class OpenAPI3_1DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : false; } @@ -516,11 +520,7 @@ class OpenAPI3_1DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = referencedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = referencedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(referencedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -532,6 +532,9 @@ class OpenAPI3_1DereferenceVisitor { linkElement: LinkElement, key: string | number, parent: Element | undefined, + path: (string | number)[], + ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // ignore LinkElement without operationRef or operationId field if (!isStringElement(linkElement.operationRef) && !isStringElement(linkElement.operationId)) { @@ -590,11 +593,7 @@ class OpenAPI3_1DereferenceVisitor { /** * Transclude Link Object containing Operation Object in its meta. */ - if (isMemberElement(parent)) { - parent.value = linkElementCopy; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = linkElementCopy; // eslint-disable-line no-param-reassign - } + link.replaceWith(linkElementCopy, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -621,11 +620,7 @@ class OpenAPI3_1DereferenceVisitor { /** * Transclude Link Object containing Operation Object in its meta. */ - if (isMemberElement(parent)) { - parent.value = linkElementCopy; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = linkElementCopy; // eslint-disable-line no-param-reassign - } + link.replaceWith(linkElementCopy, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -640,6 +635,9 @@ class OpenAPI3_1DereferenceVisitor { exampleElement: ExampleElement, key: string | number, parent: Element | undefined, + path: (string | number)[], + ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // ignore ExampleElement without externalValue field if (!isStringElement(exampleElement.externalValue)) { @@ -681,11 +679,7 @@ class OpenAPI3_1DereferenceVisitor { /** * Transclude Example Object containing external value. */ - if (isMemberElement(parent)) { - parent.value = exampleElementCopy; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = exampleElementCopy; // eslint-disable-line no-param-reassign - } + link.replaceWith(exampleElementCopy, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree. @@ -699,6 +693,7 @@ class OpenAPI3_1DereferenceVisitor { parent: Element | undefined, path: (string | number)[], ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, ) { // skip current referencing schema as $ref keyword was not defined if (!isStringElement(referencingElement.$ref)) { @@ -858,11 +853,7 @@ class OpenAPI3_1DereferenceVisitor { this.options.dereference.circularReplacer; const replacement = replacer(refElement); - if (isMemberElement(parent)) { - parent.value = replacement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replacement; // eslint-disable-line no-param-reassign - } + link.replaceWith(replacement, mutationReplacer); return !parent ? replacement : false; } @@ -925,11 +916,7 @@ class OpenAPI3_1DereferenceVisitor { cloneDeep(identityManager.identify(referencingElement)), ); - if (isMemberElement(parent)) { - parent.value = booleanJsonSchemaElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = booleanJsonSchemaElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(booleanJsonSchemaElement, mutationReplacer); return !parent ? booleanJsonSchemaElement : false; } @@ -968,11 +955,7 @@ class OpenAPI3_1DereferenceVisitor { /** * Transclude referencing element with merged referenced element. */ - if (isMemberElement(parent)) { - parent.value = referencedElement; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = referencedElement; // eslint-disable-line no-param-reassign - } + link.replaceWith(referencedElement, mutationReplacer); /** * We're at the root of the tree, so we're just replacing the entire tree.