Skip to content

Commit e14103f

Browse files
fix circular reference detection
1 parent 347e306 commit e14103f

File tree

1 file changed

+52
-22
lines changed

1 file changed

+52
-22
lines changed

src/middlewares/parsers/schema.preprocessor.ts

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,21 @@ type DiscriminatorState = {
7878
};
7979

8080
class VisitorNode<NodeType extends VisitorTypes> {
81-
public discriminator: DiscriminatorState = {};
8281
public originalRef?: string;
82+
public traversedObjects: Set<OpenAPIObject>; // track circular references
83+
public discriminator: DiscriminatorState = {};
8384

8485
constructor(
8586
public type: NodeType,
8687
public object: VisitorObjects[NodeType] | undefined,
87-
public path: string[],
88+
traversedObjects: Set<OpenAPIObject> = new Set(),
89+
public path: string[] = [],
8890
public pathFromParent?: string[],
89-
) {}
91+
) {
92+
// copy traversed object to not affect other children of the same parent
93+
this.traversedObjects = new Set(traversedObjects);
94+
this.traversedObjects.add(object);
95+
}
9096

9197
static fromParent<
9298
ParentType extends VisitorTypes,
@@ -105,6 +111,7 @@ class VisitorNode<NodeType extends VisitorTypes> {
105111
return new VisitorNode(
106112
type,
107113
parent.object[propertyPath] as unknown as VisitorObjects[NodeType],
114+
parent.traversedObjects,
108115
[...parent.path, propertyPath],
109116
);
110117
}
@@ -135,6 +142,7 @@ class VisitorNode<NodeType extends VisitorTypes> {
135142
new VisitorNode(
136143
type,
137144
value,
145+
parent.traversedObjects,
138146
[...parent.path, dictPath, key],
139147
[dictPath, key],
140148
),
@@ -170,6 +178,7 @@ class VisitorNode<NodeType extends VisitorTypes> {
170178
new VisitorNode(
171179
type,
172180
value,
181+
parent.traversedObjects,
173182
[...parent.path, arrayPath, `${index}`],
174183
[arrayPath, `${index}`],
175184
),
@@ -208,7 +217,7 @@ export class SchemaPreprocessor<
208217
}
209218

210219
public preProcess(): { apiDoc: OpenAPISchema; apiDocRes: OpenAPISchema } {
211-
const root = new VisitorNode('document', this.apiDoc, []);
220+
const root = new VisitorNode('document', this.apiDoc);
212221

213222
this.traverseSchema(root);
214223

@@ -234,23 +243,33 @@ export class SchemaPreprocessor<
234243
return;
235244
}
236245

237-
if (seenObjects.has(node.object)) return;
238-
246+
// resolve references
239247
if (isReferenceNode(node) && isReferenceObject(node.object)) {
240248
node.originalRef = node.object.$ref;
241249

250+
// TODO: Seemingly we do not want to "unreference" these schema properties.
251+
// Find way to implement this more elegantly.
252+
if (
253+
node.pathFromParent &&
254+
['allOf', 'oneOf', 'anyOf'].includes(node.pathFromParent[0]) &&
255+
hasNodeType(node, 'schema')
256+
) {
257+
this.processDiscriminator(
258+
hasNodeType(parent, 'schema') ? parent : undefined,
259+
node,
260+
);
261+
return;
262+
}
263+
242264
const resolvedObject = this.resolveObject<typeof node.type>(
243265
node.object,
244266
);
245267

246-
if (seenObjects.has(resolvedObject)) {
247-
if (hasNodeType(node, 'schema')) {
248-
this.processDiscriminator(
249-
hasNodeType(parent, 'schema') ? parent : undefined,
250-
node,
251-
);
252-
}
253-
268+
// stop when detecting circular references
269+
if (
270+
resolvedObject === undefined ||
271+
node.traversedObjects.has(resolvedObject)
272+
) {
254273
return;
255274
}
256275

@@ -271,10 +290,9 @@ export class SchemaPreprocessor<
271290
}
272291

273292
node.object = resolvedObject;
274-
275-
return traverse(parent, node as VisitorNode<NodeType>, state);
276293
}
277294

295+
if (seenObjects.has(node.object)) return;
278296
seenObjects.add(node.object);
279297

280298
this.visitNode(parent, node, state);
@@ -748,7 +766,14 @@ export class SchemaPreprocessor<
748766
...VisitorNode.fromParentDict(parent, 'pathItem', 'pathItems'),
749767
);
750768
// process components V3.1 also like normal components
751-
children.push(new VisitorNode('components', parent.object, parent.path));
769+
children.push(
770+
new VisitorNode(
771+
'components',
772+
parent.object,
773+
parent.traversedObjects,
774+
parent.path,
775+
),
776+
);
752777

753778
return children;
754779
}
@@ -786,10 +811,12 @@ export class SchemaPreprocessor<
786811
if (typeof parent.object.additionalProperties !== 'boolean') {
787812
// constructing this manually, as the type of additional properties includes boolean
788813
children.push(
789-
new VisitorNode('schema', parent.object.additionalProperties, [
790-
...parent.path,
791-
'additionalProperties',
792-
]),
814+
new VisitorNode(
815+
'schema',
816+
parent.object.additionalProperties,
817+
parent.traversedObjects,
818+
[...parent.path, 'additionalProperties'],
819+
),
793820
);
794821
}
795822
children.push(
@@ -933,7 +960,10 @@ export class SchemaPreprocessor<
933960

934961
forEachValue(parent.object, (pathItem, key) => {
935962
children.push(
936-
new VisitorNode('pathItem', pathItem, [...parent.path, key]),
963+
new VisitorNode('pathItem', pathItem, parent.traversedObjects, [
964+
...parent.path,
965+
key,
966+
]),
937967
);
938968
});
939969

0 commit comments

Comments
 (0)