should-equal.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import t from 'should-type';
  2. function format(msg) {
  3. var args = arguments;
  4. for (var i = 1, l = args.length; i < l; i++) {
  5. msg = msg.replace(/%s/, args[i]);
  6. }
  7. return msg;
  8. }
  9. var hasOwnProperty = Object.prototype.hasOwnProperty;
  10. function EqualityFail(a, b, reason, path) {
  11. this.a = a;
  12. this.b = b;
  13. this.reason = reason;
  14. this.path = path;
  15. }
  16. function typeToString(tp) {
  17. return tp.type + (tp.cls ? '(' + tp.cls + (tp.sub ? ' ' + tp.sub : '') + ')' : '');
  18. }
  19. var PLUS_0_AND_MINUS_0 = '+0 is not equal to -0';
  20. var DIFFERENT_TYPES = 'A has type %s and B has type %s';
  21. var EQUALITY = 'A is not equal to B';
  22. var EQUALITY_PROTOTYPE = 'A and B have different prototypes';
  23. var WRAPPED_VALUE = 'A wrapped value is not equal to B wrapped value';
  24. var FUNCTION_SOURCES = 'function A is not equal to B by source code value (via .toString call)';
  25. var MISSING_KEY = '%s has no key %s';
  26. var SET_MAP_MISSING_KEY = 'Set/Map missing key %s';
  27. var DEFAULT_OPTIONS = {
  28. checkProtoEql: true,
  29. checkSubType: true,
  30. plusZeroAndMinusZeroEqual: true,
  31. collectAllFails: false
  32. };
  33. function setBooleanDefault(property, obj, opts, defaults) {
  34. obj[property] = typeof opts[property] !== 'boolean' ? defaults[property] : opts[property];
  35. }
  36. var METHOD_PREFIX = '_check_';
  37. function EQ(opts, a, b, path) {
  38. opts = opts || {};
  39. setBooleanDefault('checkProtoEql', this, opts, DEFAULT_OPTIONS);
  40. setBooleanDefault('plusZeroAndMinusZeroEqual', this, opts, DEFAULT_OPTIONS);
  41. setBooleanDefault('checkSubType', this, opts, DEFAULT_OPTIONS);
  42. setBooleanDefault('collectAllFails', this, opts, DEFAULT_OPTIONS);
  43. this.a = a;
  44. this.b = b;
  45. this._meet = opts._meet || [];
  46. this.fails = opts.fails || [];
  47. this.path = path || [];
  48. }
  49. function ShortcutError(fail) {
  50. this.name = 'ShortcutError';
  51. this.message = 'fail fast';
  52. this.fail = fail;
  53. }
  54. ShortcutError.prototype = Object.create(Error.prototype);
  55. EQ.checkStrictEquality = function(a, b) {
  56. this.collectFail(a !== b, EQUALITY);
  57. };
  58. EQ.add = function add(type, cls, sub, f) {
  59. var args = Array.prototype.slice.call(arguments);
  60. f = args.pop();
  61. EQ.prototype[METHOD_PREFIX + args.join('_')] = f;
  62. };
  63. EQ.prototype = {
  64. check: function() {
  65. try {
  66. this.check0();
  67. } catch (e) {
  68. if (e instanceof ShortcutError) {
  69. return [e.fail];
  70. }
  71. throw e;
  72. }
  73. return this.fails;
  74. },
  75. check0: function() {
  76. var a = this.a;
  77. var b = this.b;
  78. // equal a and b exit early
  79. if (a === b) {
  80. // check for +0 !== -0;
  81. return this.collectFail(a === 0 && (1 / a !== 1 / b) && !this.plusZeroAndMinusZeroEqual, PLUS_0_AND_MINUS_0);
  82. }
  83. var typeA = t(a);
  84. var typeB = t(b);
  85. // if objects has different types they are not equal
  86. if (typeA.type !== typeB.type || typeA.cls !== typeB.cls || typeA.sub !== typeB.sub) {
  87. return this.collectFail(true, format(DIFFERENT_TYPES, typeToString(typeA), typeToString(typeB)));
  88. }
  89. // as types the same checks type specific things
  90. var name1 = typeA.type, name2 = typeA.type;
  91. if (typeA.cls) {
  92. name1 += '_' + typeA.cls;
  93. name2 += '_' + typeA.cls;
  94. }
  95. if (typeA.sub) {
  96. name2 += '_' + typeA.sub;
  97. }
  98. var f = this[METHOD_PREFIX + name2] || this[METHOD_PREFIX + name1] || this[METHOD_PREFIX + typeA.type] || this.defaultCheck;
  99. f.call(this, this.a, this.b);
  100. },
  101. collectFail: function(comparison, reason, showReason) {
  102. if (comparison) {
  103. var res = new EqualityFail(this.a, this.b, reason, this.path);
  104. res.showReason = !!showReason;
  105. this.fails.push(res);
  106. if (!this.collectAllFails) {
  107. throw new ShortcutError(res);
  108. }
  109. }
  110. },
  111. checkPlainObjectsEquality: function(a, b) {
  112. // compare deep objects and arrays
  113. // stacks contain references only
  114. //
  115. var meet = this._meet;
  116. var m = this._meet.length;
  117. while (m--) {
  118. var st = meet[m];
  119. if (st[0] === a && st[1] === b) {
  120. return;
  121. }
  122. }
  123. // add `a` and `b` to the stack of traversed objects
  124. meet.push([a, b]);
  125. // TODO maybe something else like getOwnPropertyNames
  126. var key;
  127. for (key in b) {
  128. if (hasOwnProperty.call(b, key)) {
  129. if (hasOwnProperty.call(a, key)) {
  130. this.checkPropertyEquality(key);
  131. } else {
  132. this.collectFail(true, format(MISSING_KEY, 'A', key));
  133. }
  134. }
  135. }
  136. // ensure both objects have the same number of properties
  137. for (key in a) {
  138. if (hasOwnProperty.call(a, key)) {
  139. this.collectFail(!hasOwnProperty.call(b, key), format(MISSING_KEY, 'B', key));
  140. }
  141. }
  142. meet.pop();
  143. if (this.checkProtoEql) {
  144. //TODO should i check prototypes for === or use eq?
  145. this.collectFail(Object.getPrototypeOf(a) !== Object.getPrototypeOf(b), EQUALITY_PROTOTYPE, true);
  146. }
  147. },
  148. checkPropertyEquality: function(propertyName) {
  149. var _eq = new EQ(this, this.a[propertyName], this.b[propertyName], this.path.concat([propertyName]));
  150. _eq.check0();
  151. },
  152. defaultCheck: EQ.checkStrictEquality
  153. };
  154. EQ.add(t.NUMBER, function(a, b) {
  155. this.collectFail((a !== a && b === b) || (b !== b && a === a) || (a !== b && a === a && b === b), EQUALITY);
  156. });
  157. [t.SYMBOL, t.BOOLEAN, t.STRING].forEach(function(tp) {
  158. EQ.add(tp, EQ.checkStrictEquality);
  159. });
  160. EQ.add(t.FUNCTION, function(a, b) {
  161. // functions are compared by their source code
  162. this.collectFail(a.toString() !== b.toString(), FUNCTION_SOURCES);
  163. // check user properties
  164. this.checkPlainObjectsEquality(a, b);
  165. });
  166. EQ.add(t.OBJECT, t.REGEXP, function(a, b) {
  167. // check regexp flags
  168. var flags = ['source', 'global', 'multiline', 'lastIndex', 'ignoreCase', 'sticky', 'unicode'];
  169. while (flags.length) {
  170. this.checkPropertyEquality(flags.shift());
  171. }
  172. // check user properties
  173. this.checkPlainObjectsEquality(a, b);
  174. });
  175. EQ.add(t.OBJECT, t.DATE, function(a, b) {
  176. //check by timestamp only (using .valueOf)
  177. this.collectFail(+a !== +b, EQUALITY);
  178. // check user properties
  179. this.checkPlainObjectsEquality(a, b);
  180. });
  181. [t.NUMBER, t.BOOLEAN, t.STRING].forEach(function(tp) {
  182. EQ.add(t.OBJECT, tp, function(a, b) {
  183. //primitive type wrappers
  184. this.collectFail(a.valueOf() !== b.valueOf(), WRAPPED_VALUE);
  185. // check user properties
  186. this.checkPlainObjectsEquality(a, b);
  187. });
  188. });
  189. EQ.add(t.OBJECT, function(a, b) {
  190. this.checkPlainObjectsEquality(a, b);
  191. });
  192. [t.ARRAY, t.ARGUMENTS, t.TYPED_ARRAY].forEach(function(tp) {
  193. EQ.add(t.OBJECT, tp, function(a, b) {
  194. this.checkPropertyEquality('length');
  195. this.checkPlainObjectsEquality(a, b);
  196. });
  197. });
  198. EQ.add(t.OBJECT, t.ARRAY_BUFFER, function(a, b) {
  199. this.checkPropertyEquality('byteLength');
  200. this.checkPlainObjectsEquality(a, b);
  201. });
  202. EQ.add(t.OBJECT, t.ERROR, function(a, b) {
  203. this.checkPropertyEquality('name');
  204. this.checkPropertyEquality('message');
  205. this.checkPlainObjectsEquality(a, b);
  206. });
  207. EQ.add(t.OBJECT, t.BUFFER, function(a) {
  208. this.checkPropertyEquality('length');
  209. var l = a.length;
  210. while (l--) {
  211. this.checkPropertyEquality(l);
  212. }
  213. //we do not check for user properties because
  214. //node Buffer have some strange hidden properties
  215. });
  216. [t.MAP, t.SET, t.WEAK_MAP, t.WEAK_SET].forEach(function(tp) {
  217. EQ.add(t.OBJECT, tp, function(a, b) {
  218. this._meet.push([a, b]);
  219. var iteratorA = a.entries();
  220. for (var nextA = iteratorA.next(); !nextA.done; nextA = iteratorA.next()) {
  221. var iteratorB = b.entries();
  222. var keyFound = false;
  223. for (var nextB = iteratorB.next(); !nextB.done; nextB = iteratorB.next()) {
  224. // try to check keys first
  225. var r = eq(nextA.value[0], nextB.value[0], { collectAllFails: false, _meet: this._meet });
  226. if (r.length === 0) {
  227. keyFound = true;
  228. // check values also
  229. eq(nextA.value[1], nextB.value[1], this);
  230. }
  231. }
  232. if (!keyFound) {
  233. // no such key at all
  234. this.collectFail(true, format(SET_MAP_MISSING_KEY, nextA.value[0]));
  235. }
  236. }
  237. this._meet.pop();
  238. this.checkPlainObjectsEquality(a, b);
  239. });
  240. });
  241. function eq(a, b, opts) {
  242. return new EQ(opts, a, b).check();
  243. }
  244. eq.EQ = EQ;
  245. export default eq;