forked from chaijs/chai
/
proxify.js
127 lines (114 loc) · 4.83 KB
/
proxify.js
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
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 <jake@alogicalparadox.com>
* 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 + '".');
}
var orderedProperties = getProperties(target).filter(function(property) {
return !Object.prototype.hasOwnProperty(property) &&
builtins.indexOf(property) === -1;
}).sort(function(a, b) {
return stringDistance(property, a) - stringDistance(property, b);
});
if (orderedProperties.length &&
stringDistance(orderedProperties[0], property) < 4) {
// If the property is reasonably close to an existing Chai property,
// suggest that property to the user.
throw Error('Invalid Chai property: ' + property +
'. Did you mean "' + orderedProperties[0] + '"?');
} 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);
}
});
};
/**
* # stringDistance(strA, strB)
* Return the Levenshtein distance between two strings.
* @param {string} strA
* @param {string} strB
* @return {number} the string distance between strA and strB
* @api private
*/
function stringDistance(strA, strB, memo) {
if (!memo) {
// `memo` is a two-dimensional array containing a cache of distances
// memo[i][j] is the distance between strA.slice(0, i) and
// strB.slice(0, j).
memo = [];
for (var i = 0; i <= strA.length; i++) {
memo[i] = [];
}
}
if (!memo[strA.length] || !memo[strA.length][strB.length]) {
if (strA.length === 0 || strB.length === 0) {
memo[strA.length][strB.length] = Math.max(strA.length, strB.length);
} else {
var sliceA = strA.slice(0, -1);
var sliceB = strB.slice(0, -1);
memo[strA.length][strB.length] = Math.min(
stringDistance(sliceA, strB, memo) + 1,
stringDistance(strA, sliceB, memo) + 1,
stringDistance(sliceA, sliceB, memo) +
(strA.slice(-1) === strB.slice(-1) ? 0 : 1)
);
}
}
return memo[strA.length][strB.length];
}