diff --git a/lib/resolve-external.js b/lib/resolve-external.js index c7238fbd..0d0a8648 100644 --- a/lib/resolve-external.js +++ b/lib/resolve-external.js @@ -44,6 +44,7 @@ function resolveExternal (parser, options) { * @param {string} path - The full path of `obj`, possibly with a JSON Pointer in the hash * @param {$Refs} $refs * @param {$RefParserOptions} options + * @param {Set} seen - Internal. * * @returns {Promise[]} * Returns an array of promises. There will be one promise for each JSON reference in `obj`. @@ -51,10 +52,12 @@ function resolveExternal (parser, options) { * If any of the JSON references point to files that contain additional JSON references, * then the corresponding promise will internally reference an array of promises. */ -function crawl (obj, path, $refs, options) { +function crawl (obj, path, $refs, options, seen) { + seen = seen || new Set(); let promises = []; - if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) { + if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) { + seen.add(obj); // Track previously seen objects to avoid infinite recursion if ($Ref.isExternal$Ref(obj)) { promises.push(resolve$Ref(obj, path, $refs, options)); } @@ -67,7 +70,7 @@ function crawl (obj, path, $refs, options) { promises.push(resolve$Ref(value, keyPath, $refs, options)); } else { - promises = promises.concat(crawl(value, keyPath, $refs, options)); + promises = promises.concat(crawl(value, keyPath, $refs, options, seen)); } } } diff --git a/test/specs/circular/circular.spec.js b/test/specs/circular/circular.spec.js index 0cbef6df..2ba5b886 100644 --- a/test/specs/circular/circular.spec.js +++ b/test/specs/circular/circular.spec.js @@ -36,6 +36,18 @@ describe("Schema with circular (recursive) $refs", () => { expect(schema.definitions.child.properties.pet).to.equal(schema.definitions.pet); }); + it("should double dereference successfully", async () => { + const firstPassSchema = await $RefParser.dereference(path.rel("specs/circular/circular-self.yaml")); + let parser = new $RefParser(); + const schema = await parser.dereference(firstPassSchema); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(dereferencedSchema.self); + // The "circular" flag should be set + expect(parser.$refs.circular).to.equal(true); + // Reference equality + expect(schema.definitions.child.properties.pet).to.equal(schema.definitions.pet); + }); + it('should produce the same results if "options.$refs.circular" is "ignore"', async () => { let parser = new $RefParser(); const schema = await parser.dereference(path.rel("specs/circular/circular-self.yaml"), { dereference: { circular: "ignore" }}); @@ -103,6 +115,19 @@ describe("Schema with circular (recursive) $refs", () => { expect(schema.definitions.person.properties.pet).to.equal(schema.definitions.pet); }); + it("should double dereference successfully", async () => { + let parser = new $RefParser(); + const firstPassSchema = await $RefParser.dereference(path.rel("specs/circular/circular-ancestor.yaml")); + const schema = await parser.dereference(firstPassSchema); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(dereferencedSchema.ancestor.fullyDereferenced); + // The "circular" flag should be set + expect(parser.$refs.circular).to.equal(true); + // Reference equality + expect(schema.definitions.person.properties.spouse).to.equal(schema.definitions.person); + expect(schema.definitions.person.properties.pet).to.equal(schema.definitions.pet); + }); + it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => { let parser = new $RefParser(); const schema = await parser.dereference(path.rel("specs/circular/circular-ancestor.yaml"), { dereference: { circular: "ignore" }}); @@ -174,6 +199,21 @@ describe("Schema with circular (recursive) $refs", () => { .to.equal(schema.definitions.parent); }); + it("should double dereference successfully", async () => { + let parser = new $RefParser(); + const firstPassSchema = await $RefParser.dereference(path.rel("specs/circular/circular-indirect.yaml")); + const schema = await parser.dereference(firstPassSchema); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(dereferencedSchema.indirect.fullyDereferenced); + // The "circular" flag should be set + expect(parser.$refs.circular).to.equal(true); + // Reference equality + expect(schema.definitions.parent.properties.children.items) + .to.equal(schema.definitions.child); + expect(schema.definitions.child.properties.parents.items) + .to.equal(schema.definitions.parent); + }); + it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => { let parser = new $RefParser(); const schema = await parser.dereference(path.rel("specs/circular/circular-indirect.yaml"), { dereference: { circular: "ignore" }}); @@ -245,6 +285,21 @@ describe("Schema with circular (recursive) $refs", () => { .to.equal(schema.definitions.child); }); + it("should double dereference successfully", async () => { + let parser = new $RefParser(); + const firstPassSchema = await parser.dereference(path.rel("specs/circular/circular-indirect-ancestor.yaml")); + const schema = await parser.dereference(firstPassSchema); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(dereferencedSchema.indirectAncestor.fullyDereferenced); + // The "circular" flag should be set + expect(parser.$refs.circular).to.equal(true); + // Reference equality + expect(schema.definitions.parent.properties.child) + .to.equal(schema.definitions.child); + expect(schema.definitions.child.properties.children.items) + .to.equal(schema.definitions.child); + }); + it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => { let parser = new $RefParser(); const schema = await parser.dereference(path.rel("specs/circular/circular-indirect-ancestor.yaml"), { dereference: { circular: "ignore" }});