All files / src/cli-validator/utils circular-references-ibm.js

96.83% Statements 61/63
86.36% Branches 19/22
100% Functions 8/8
96.72% Lines 59/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137      28x 28x     28x 5x 5x 5x 5x 5x 5x 8x         5x         28x 6x     761x         761x 448x     313x   313x       313x 764x 258x 9x 9x 9x     755x       6x 6x         28x 6x 6x 9x 9x     9x 79x 79x     79x           79x 79x 72x     72x   7x   37x       7x       7x 7x 14x 14x           14x 2x               7x 2x 2x         5x       77x   9x     6x     28x 28x 28x  
// find the circular references,
// correct them,
 
const isPlainObject = require('lodash/isPlainObject');
const isObject = require('lodash/isObject');
 
// and return them as problems if applicable
const validate = function({ jsSpec, resolvedSpec }, config) {
  const result = { error: [], warning: [] };
  config = config.walker;
  const pathsInResolvedSpec = correctSpec(resolvedSpec);
  const actualPaths = convertPaths(jsSpec, pathsInResolvedSpec);
  const checkStatus = config.has_circular_references;
  Eif (checkStatus !== 'off') {
    result[checkStatus] = actualPaths.map(path => ({
      message: 'Swagger object should not contain circular references.',
      path
    }));
  }
  return { errors: result.error, warnings: result.warning };
};
 
// this function finds circular references in the resolved spec and
// cuts them off to allow recursive walks in the validators
const correctSpec = function(resolvedSpec) {
  const paths = [];
 
  function walk(object, path, visitedObjects) {
    Iif (object === null) {
      return null;
    }
 
    // we need to catch arrays here, in addition to plain objects
    if (!isObject(object)) {
      return null;
    }
 
    const keys = Object.keys(object);
 
    Iif (!keys.length) {
      return null;
    }
 
    return keys.forEach(function(key) {
      if (isPlainObject(object[key])) {
        if (visitedObjects.includes(object[key])) {
          paths.push([...path, key]);
          object[key] = '[Circular]';
          return;
        }
      }
      return walk(object[key], [...path, key], [...visitedObjects, object]);
    });
  }
 
  walk(resolvedSpec, [], []);
  return paths;
};
 
// this function takes the paths found while correcting the spec and
// finds where the circular references are happening in the actual spec
const convertPaths = function(jsSpec, resolvedPaths) {
  const realPaths = [];
  resolvedPaths.forEach(path => {
    let realPath = [];
    let previous = jsSpec;
 
    // path is an array of keys leading to the circular reference
    for (let i = 0; i < path.length; i++) {
      const key = path[i];
      realPath.push(key);
 
      // access the next nested object using the given key
      let current = previous[key];
 
      // if this is the last key, don't follow the ref object
      // or else the result will be only the object path
      //  (i.e. "definitions.QueryAggregation")
      // instead of the path to where the circular reference occurs
      const lastKey = i === path.length - 1;
      if (!lastKey) {
        const nextKey = path[i + 1];
 
        // Only follow $ref if the next key is not present
        if (!current[nextKey] && current.$ref) {
          // locationArray holds the keys to the references object in the spec
          const locationArray = current.$ref
            .split('/')
            .filter(refKey => refKey !== '#');
 
          // since we are following a ref to the first object level,
          // realPath needs to be reset
          realPath = [...locationArray];
 
          // to follow the keys to the ref object we need to start looking in,
          // a mini-version of the parent loop is necessary
          let refPrevious = jsSpec;
          for (const refKey of locationArray) {
            const refCurrent = refPrevious[refKey];
            refPrevious = refCurrent;
 
            // this should only happen in a specific multi-file spec scenario - schemas across
            // files that reference one another (creating a circular reference)
            // the bundler doesn't handle this situation well, so there isn't a lot we can do other than
            // break out of the loop to avoid any runtime errors
            if (!refPrevious) {
              break;
            }
          }
 
          // as mentioned above, this should only happen in rare, multi-file spec scenarios.
          // break from the loop, then go ahead and return the resolved path to the user,
          // who will have to manually trace it to the source
          // there is not really a better way to handle it without major changes
          if (!refPrevious) {
            realPath = path;
            break;
          }
 
          // set the parent current object to the object found at the
          // referenced path to continue using the keys in the parent loop
          current = refPrevious;
        }
      }
 
      previous = current;
    }
    realPaths.push(realPath.join('.'));
  });
 
  return realPaths;
};
 
module.exports.validate = validate;
module.exports.correct = correctSpec;
module.exports.convert = convertPaths;