var config = require('../config'); var flag = require('./flag'); var getProperties = require('./getProperties'); var isProxyEnabled = require('./isProxyEnabled'); /*! * Chai - proxify utility * Copyright(c) 2012-2014 Jake Luer * MIT Licensed */ /** * ### .proxify(object) * * Return a proxy of given object that throws an error when a non-existent * property is read. By default, the root cause is assumed to be a misspelled * property, and thus an attempt is made to offer a reasonable suggestion from * the list of existing properties. However, if a nonChainableMethodName is * provided, then the root cause is instead a failure to invoke a non-chainable * method prior to reading the non-existent property. * * If proxies are unsupported or disabled via the user's Chai config, then * return object without modification. * * @param {Object} obj * @param {String} nonChainableMethodName * @namespace Utils * @name proxify */ var builtins = ['__flags', '__methods', '_obj', 'assert']; module.exports = function proxify(obj, nonChainableMethodName) { if (!isProxyEnabled()) return obj; return new Proxy(obj, { get: function proxyGetter(target, property) { // This check is here because we should not throw errors on Symbol properties // such as `Symbol.toStringTag`. // The values for which an error should be thrown can be configured using // the `config.proxyExcludedKeys` setting. if (typeof property === 'string' && config.proxyExcludedKeys.indexOf(property) === -1 && !Reflect.has(target, property)) { // Special message for invalid property access of non-chainable methods. if (nonChainableMethodName) { throw Error('Invalid Chai property: ' + nonChainableMethodName + '.' + property + '. See docs for proper usage of "' + nonChainableMethodName + '".'); } // If the property is reasonably close to an existing Chai property, // suggest that property to the user. Only suggest properties with a // distance less than 4. var suggestion = null; var suggestionDistance = 4; getProperties(target).forEach(function(prop) { if ( !Object.prototype.hasOwnProperty(prop) && builtins.indexOf(prop) === -1 ) { var dist = stringDistanceCapped( property, prop, suggestionDistance ); if (dist < suggestionDistance) { suggestion = prop; suggestionDistance = dist; } } }); if (suggestion !== null) { throw Error('Invalid Chai property: ' + property + '. Did you mean "' + suggestion + '"?'); } else { throw Error('Invalid Chai property: ' + property); } } // Use this proxy getter as the starting point for removing implementation // frames from the stack trace of a failed assertion. For property // assertions, this prevents the proxy getter from showing up in the stack // trace since it's invoked before the property getter. For method and // chainable method assertions, this flag will end up getting changed to // the method wrapper, which is good since this frame will no longer be in // the stack once the method is invoked. Note that Chai builtin assertion // properties such as `__flags` are skipped since this is only meant to // capture the starting point of an assertion. This step is also skipped // if the `lockSsfi` flag is set, thus indicating that this assertion is // being called from within another assertion. In that case, the `ssfi` // flag is already set to the outer assertion's starting point. if (builtins.indexOf(property) === -1 && !flag(target, 'lockSsfi')) { flag(target, 'ssfi', proxyGetter); } return Reflect.get(target, property); } }); }; /** * # stringDistanceCapped(strA, strB, cap) * Return the Levenshtein distance between two strings, but no more than cap. * @param {string} strA * @param {string} strB * @param {number} number * @return {number} min(string distance between strA and strB, cap) * @api private */ function stringDistanceCapped(strA, strB, cap) { if (Math.abs(strA.length - strB.length) >= cap) { return cap; } var memo = []; // `memo` is a two-dimensional array containing distances. // memo[i][j] is the distance between strA.slice(0, i) and // strB.slice(0, j). for (var i = 0; i <= strA.length; i++) { memo[i] = Array(strB.length + 1).fill(0); memo[i][0] = i; } for (var j = 0; j < strB.length; j++) { memo[0][j] = j; } for (var i = 1; i <= strA.length; i++) { var ch = strA.charCodeAt(i - 1); for (var j = 1; j <= strB.length; j++) { if (Math.abs(i - j) >= cap) { memo[i][j] = cap; continue; } memo[i][j] = Math.min( memo[i - 1][j] + 1, memo[i][j - 1] + 1, memo[i - 1][j - 1] + (ch === strB.charCodeAt(j - 1) ? 0 : 1) ); } } return memo[strA.length][strB.length]; }