diff --git a/lib/dereference.js b/lib/dereference.js index 4eeb82dd..ca338f25 100644 --- a/lib/dereference.js +++ b/lib/dereference.js @@ -16,7 +16,7 @@ module.exports = dereference; */ function dereference (parser, options) { // console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path); - let dereferenced = crawl(parser.schema, parser.$refs._root$Ref.path, "#", [], parser.$refs, options); + let dereferenced = crawl(parser.schema, parser.$refs._root$Ref.path, "#", [], [], {}, parser.$refs, options); parser.$refs.circular = dereferenced.circular; parser.schema = dereferenced.value; } @@ -28,43 +28,38 @@ function dereference (parser, options) { * @param {string} path - The full path of `obj`, possibly with a JSON Pointer in the hash * @param {string} pathFromRoot - The path of `obj` from the schema root * @param {object[]} parents - An array of the parent objects that have already been dereferenced + * @param {object[]} processedObjects - An array of all the objects that have already been processed + * @param {object} dereferencedCache - An map of all the dereferenced objects * @param {$Refs} $refs * @param {$RefParserOptions} options * @returns {{value: object, circular: boolean}} */ -function crawl (obj, path, pathFromRoot, parents, $refs, options) { +function crawl (obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options) { let dereferenced; let result = { value: obj, circular: false }; - if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) { - parents.push(obj); + if (options.dereference.circular === "ignore" || processedObjects.indexOf(obj) === -1) { + if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) { + parents.push(obj); + processedObjects.push(obj); - if ($Ref.isAllowed$Ref(obj, options)) { - dereferenced = dereference$Ref(obj, path, pathFromRoot, parents, $refs, options); - result.circular = dereferenced.circular; - result.value = dereferenced.value; - } - else { - for (let key of Object.keys(obj)) { - let keyPath = Pointer.join(path, key); - let keyPathFromRoot = Pointer.join(pathFromRoot, key); - let value = obj[key]; - let circular = false; - - if ($Ref.isAllowed$Ref(value, options)) { - dereferenced = dereference$Ref(value, keyPath, keyPathFromRoot, parents, $refs, options); - circular = dereferenced.circular; - // Avoid pointless mutations; breaks frozen objects to no profit - if (obj[key] !== dereferenced.value) { - obj[key] = dereferenced.value; - } - } - else { - if (parents.indexOf(value) === -1) { - dereferenced = crawl(value, keyPath, keyPathFromRoot, parents, $refs, options); + if ($Ref.isAllowed$Ref(obj, options)) { + dereferenced = dereference$Ref(obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options); + result.circular = dereferenced.circular; + result.value = dereferenced.value; + } + else { + for (let key of Object.keys(obj)) { + let keyPath = Pointer.join(path, key); + let keyPathFromRoot = Pointer.join(pathFromRoot, key); + let value = obj[key]; + let circular = false; + + if ($Ref.isAllowed$Ref(value, options)) { + dereferenced = dereference$Ref(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options); circular = dereferenced.circular; // Avoid pointless mutations; breaks frozen objects to no profit if (obj[key] !== dereferenced.value) { @@ -72,16 +67,26 @@ function crawl (obj, path, pathFromRoot, parents, $refs, options) { } } else { - circular = foundCircularReference(keyPath, $refs, options); + if (parents.indexOf(value) === -1) { + dereferenced = crawl(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options); + circular = dereferenced.circular; + // Avoid pointless mutations; breaks frozen objects to no profit + if (obj[key] !== dereferenced.value) { + obj[key] = dereferenced.value; + } + } + else { + circular = foundCircularReference(keyPath, $refs, options); + } } - } - // Set the "isCircular" flag if this or any other property is circular - result.circular = result.circular || circular; + // Set the "isCircular" flag if this or any other property is circular + result.circular = result.circular || circular; + } } - } - parents.pop(); + parents.pop(); + } } return result; @@ -94,14 +99,38 @@ function crawl (obj, path, pathFromRoot, parents, $refs, options) { * @param {string} path - The full path of `$ref`, possibly with a JSON Pointer in the hash * @param {string} pathFromRoot - The path of `$ref` from the schema root * @param {object[]} parents - An array of the parent objects that have already been dereferenced + * @param {object[]} processedObjects - An array of all the objects that have already been dereferenced + * @param {object} dereferencedCache - An map of all the dereferenced objects * @param {$Refs} $refs * @param {$RefParserOptions} options * @returns {{value: object, circular: boolean}} */ -function dereference$Ref ($ref, path, pathFromRoot, parents, $refs, options) { +function dereference$Ref ($ref, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options) { // console.log('Dereferencing $ref pointer "%s" at %s', $ref.$ref, path); let $refPath = url.resolve(path, $ref.$ref); + + if (dereferencedCache[$refPath]) { + const cache = dereferencedCache[$refPath]; + + const refKeys = Object.keys($ref); + if (refKeys.length > 1) { + const extraKeys = {}; + for (let key of refKeys) { + if (key !== "$ref" && !(key in cache.value)) { + extraKeys[key] = $ref[key]; + } + } + return { + circular: cache.circular, + value: Object.assign({}, cache.value, extraKeys), + }; + } + + return cache; + } + + let pointer = $refs._resolve($refPath, path, options); if (pointer === null) { @@ -122,7 +151,7 @@ function dereference$Ref ($ref, path, pathFromRoot, parents, $refs, options) { // Crawl the dereferenced value (unless it's circular) if (!circular) { // Determine if the dereferenced value is circular - let dereferenced = crawl(dereferencedValue, pointer.path, pathFromRoot, parents, $refs, options); + let dereferenced = crawl(dereferencedValue, pointer.path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options); circular = dereferenced.circular; dereferencedValue = dereferenced.value; } @@ -138,10 +167,18 @@ function dereference$Ref ($ref, path, pathFromRoot, parents, $refs, options) { dereferencedValue.$ref = pathFromRoot; } - return { + + const dereferencedObject = { circular, value: dereferencedValue }; + + // only cache if no extra properties than $ref + if (Object.keys($ref).length === 1) { + dereferencedCache[$refPath] = dereferencedObject; + } + + return dereferencedObject; } /**