From ffb2522eb94afe46a8ccc23faebdcee23ea6f170 Mon Sep 17 00:00:00 2001 From: Kotaro Hirayama Date: Wed, 18 Apr 2018 17:59:17 +0900 Subject: [PATCH 1/6] [Breaking] ensure `Error` objects compare properly --- .eslintrc | 1 + index.js | 10 +++++++++- test/cmp.js | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 4ff2c50..4fb86e1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,7 @@ "files": ["example/**", "test/**"], "rules": { "array-bracket-newline": 0, + "max-lines": 0, "max-params": 0, "max-statements": 0, "no-console": 0, diff --git a/index.js b/index.js index 61307f3..d980db2 100644 --- a/index.js +++ b/index.js @@ -50,7 +50,7 @@ function isBuffer(x) { } function objEquiv(a, b, opts) { - /* eslint max-statements: [2, 50] */ + /* eslint max-statements: [2, 70] */ var i, key; if (typeof a !== typeof b) { return false; } if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { return false; } @@ -60,6 +60,14 @@ function objEquiv(a, b, opts) { if (isArguments(a) !== isArguments(b)) { return false; } + // TODO: replace when a cross-realm brand check is available + var aIsError = a instanceof Error; + var bIsError = b instanceof Error; + if (aIsError !== bIsError) { return false; } + if (aIsError || bIsError) { + if (a.name !== b.name || a.message !== b.message) { return false; } + } + var aIsRegex = isRegex(a); var bIsRegex = isRegex(b); if (aIsRegex !== bIsRegex) { return false; } diff --git a/test/cmp.js b/test/cmp.js index b377743..538e827 100644 --- a/test/cmp.js +++ b/test/cmp.js @@ -342,3 +342,11 @@ test('functions', function (t) { t.end(); }); + +test('Errors', function (t) { + t.deepEqualTest(new Error('xyz'), new Error('xyz'), 'two errors of the same type with the same message', true, true, false); + t.deepEqualTest(new Error('xyz'), new TypeError('xyz'), 'two errors of different types with the same message', false, false); + t.deepEqualTest(new Error('xyz'), new Error('zyx'), 'two errors of the same type with a different message', false, false); + + t.end(); +}); From c0ae38c43872621da83aba608cd62b951de7c4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20=C3=96rn=20S=C3=ADmonarson?= Date: Mon, 24 Apr 2017 21:46:33 +0000 Subject: [PATCH 2/6] [Tests] add more Error tests --- package.json | 1 + test/cmp.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/package.json b/package.json index f580769..cfabbfc 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@ljharb/eslint-config": "^13.1.1", "eslint": "^5.16.0", + "object.assign": "^4.1.0", "tape": "^4.11.0" }, "repository": { diff --git a/test/cmp.js b/test/cmp.js index 538e827..f540228 100644 --- a/test/cmp.js +++ b/test/cmp.js @@ -1,5 +1,6 @@ var test = require('tape'); require('./_tape'); +var assign = require('object.assign'); var equal = require('../'); @@ -348,5 +349,23 @@ test('Errors', function (t) { t.deepEqualTest(new Error('xyz'), new TypeError('xyz'), 'two errors of different types with the same message', false, false); t.deepEqualTest(new Error('xyz'), new Error('zyx'), 'two errors of the same type with a different message', false, false); + t.deepEqualTest( + new Error('a'), + assign(new Error('a'), { code: 10 }), + 'two otherwise equal errors with different own properties', + false, + false + ); + + t.end(); +}); + +test('errors', function (t) { + + t.end(); +}); + +test('error = Object', function (t) { + t.notOk(equal(new Error('a'), { message: 'a' })); t.end(); }); From 3fea9da59947dc16c61be3312b3afdc293153086 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 30 Jul 2019 20:38:45 -0700 Subject: [PATCH 3/6] =?UTF-8?q?[Breaking]=20comparing=20arrays=20and=20obj?= =?UTF-8?q?ects=20should=20always=20fail,=20per=20node=E2=80=98s=20assert.?= =?UTF-8?q?deepEqual?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #12. Relates to https://github.com/substack/tape/issues/186 --- index.js | 5 +++++ package.json | 1 + test/cmp.js | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index d980db2..99eb30e 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ var isArguments = require('is-arguments'); var is = require('object-is'); var isRegex = require('is-regex'); var flags = require('regexp.prototype.flags'); +var isArray = require('isarray'); var isDate = require('is-date-object'); var getTime = Date.prototype.getTime; @@ -60,6 +61,10 @@ function objEquiv(a, b, opts) { if (isArguments(a) !== isArguments(b)) { return false; } + var aIsArray = isArray(a); + var bIsArray = isArray(b); + if (aIsArray !== bIsArray) { return false; } + // TODO: replace when a cross-realm brand check is available var aIsError = a instanceof Error; var bIsError = b instanceof Error; diff --git a/package.json b/package.json index cfabbfc..b442279 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "is-arguments": "^1.0.4", "is-date-object": "^1.0.1", "is-regex": "^1.0.4", + "isarray": "^2.0.5", "object-is": "^1.0.1", "object-keys": "^1.1.1", "regexp.prototype.flags": "^1.2.0" diff --git a/test/cmp.js b/test/cmp.js index f540228..3b852af 100644 --- a/test/cmp.js +++ b/test/cmp.js @@ -328,9 +328,9 @@ test('regexen', function (t) { }); test('arrays and objects', function (t) { - t.deepEqualTest([], {}, 'empty array and empty object', true, true); + t.deepEqualTest([], {}, 'empty array and empty object', false, false); t.deepEqualTest([], { length: 0 }, 'empty array and empty arraylike object', false, false); - t.deepEqualTest([1], { 0: 1 }, 'array and similar object', true, true); + t.deepEqualTest([1], { 0: 1 }, 'array and similar object', false, false); t.end(); }); From 5f2f2e55a3cfea251ca502a7846489a9133b4347 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 30 Jul 2019 21:02:42 -0700 Subject: [PATCH 4/6] [Breaking] non-Date objects with different [[Prototypes]] are not equal Date objects are equal when: - the timestamp is the same - if strict, the [[Prototype]] is the same - own properties match, same as normal objects --- index.js | 11 ++++++++--- test/cmp.js | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 99eb30e..ece8d96 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ var isArray = require('isarray'); var isDate = require('is-date-object'); var getTime = Date.prototype.getTime; +var gPO = Object.getPrototypeOf; function deepEqual(actual, expected, options) { var opts = options || {}; @@ -80,9 +81,13 @@ function objEquiv(a, b, opts) { return a.source === b.source && flags(a) === flags(b); } - if (isDate(a) && isDate(b)) { - return getTime.call(a) === getTime.call(b); - } + var aIsDate = isDate(a); + var bIsDate = isDate(b); + if (aIsDate !== bIsDate) { return false; } + if (aIsDate || bIsDate) { // && would work too, because both are true or both false here + if (getTime.call(a) !== getTime.call(b)) { return false; } + if (opts.strict && gPO && gPO(a) !== gPO(b)) { return false; } + } else if (gPO && gPO(a) !== gPO(b)) { return false; } // non-Dates always compare [[Prototype]]s var aIsBuffer = isBuffer(a); var bIsBuffer = isBuffer(b); diff --git a/test/cmp.js b/test/cmp.js index 3b852af..567cd93 100644 --- a/test/cmp.js +++ b/test/cmp.js @@ -91,7 +91,13 @@ test('arguments class', function (t) { test('dates', function (t) { var d0 = new Date(1387585278000); var d1 = new Date('Fri Dec 20 2013 16:21:18 GMT-0800 (PST)'); - t.deepEqualTest(d0, d1, 'equivalent Dates', true, true); + + t.deepEqualTest(d0, d1, 'two Dates with the same timestamp', true, true); + + d1.a = true; + + t.deepEqualTest(d0, d1, 'two Dates with the same timestamp but different own properties', false, false); + t.end(); }); @@ -369,3 +375,29 @@ test('error = Object', function (t) { t.notOk(equal(new Error('a'), { message: 'a' })); t.end(); }); + +test('[[Prototypes]]', { skip: !Object.getPrototypeOf }, function (t) { + function C() {} + var instance = new C(); + delete instance.constructor; + + t.deepEqualTest({}, instance, 'two identical objects with different [[Prototypes]]', false, false); + + t.test('Dates with different prototypes', { skip: !Object.setPrototypeOf }, function (st) { + var d1 = new Date(0); + var d2 = new Date(0); + + t.deepEqualTest(d1, d2, 'two dates with the same timestamp', true, true); + + var newProto = {}; + Object.setPrototypeOf(newProto, Date.prototype); + Object.setPrototypeOf(d2, newProto); + st.ok(d2 instanceof Date, 'd2 is still a Date instance after tweaking [[Prototype]]'); + + t.deepEqualTest(d1, d2, 'two dates with the same timestamp and different [[Prototype]]', true, false); + + st.end(); + }); + + t.end(); +}); From 45431b67b5e7e9cb91d5ff8706dc55348d59184e Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 6 Aug 2019 13:20:24 -0700 Subject: [PATCH 5/6] [Breaking] two objects with different `Symbol.toStringTag`s are not equal --- index.js | 3 +++ package.json | 1 + test/cmp.js | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/index.js b/index.js index ece8d96..35dcdb3 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ var isDate = require('is-date-object'); var getTime = Date.prototype.getTime; var gPO = Object.getPrototypeOf; +var objToString = Object.prototype.toString; function deepEqual(actual, expected, options) { var opts = options || {}; @@ -60,6 +61,8 @@ function objEquiv(a, b, opts) { // an identical 'prototype' property. if (a.prototype !== b.prototype) { return false; } + if (objToString.call(a) !== objToString.call(b)) { return false; } + if (isArguments(a) !== isArguments(b)) { return false; } var aIsArray = isArray(a); diff --git a/package.json b/package.json index b442279..8055cc1 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@ljharb/eslint-config": "^13.1.1", "eslint": "^5.16.0", + "has-symbols": "^1.0.0", "object.assign": "^4.1.0", "tape": "^4.11.0" }, diff --git a/test/cmp.js b/test/cmp.js index 567cd93..49ad67a 100644 --- a/test/cmp.js +++ b/test/cmp.js @@ -1,6 +1,7 @@ var test = require('tape'); require('./_tape'); var assign = require('object.assign'); +var hasSymbols = require('has-symbols')(); var equal = require('../'); @@ -401,3 +402,20 @@ test('[[Prototypes]]', { skip: !Object.getPrototypeOf }, function (t) { t.end(); }); + +test('toStringTag', { skip: !hasSymbols || !Symbol.toStringTag }, function (t) { + var o1 = {}; + t.equal(Object.prototype.toString.call(o1), '[object Object]', 'o1: Symbol.toStringTag works'); + + var o2 = {}; + t.equal(Object.prototype.toString.call(o2), '[object Object]', 'o2: original Symbol.toStringTag works'); + + t.deepEqualTest(o1, o2, 'two normal empty objects', true, true); + + o2[Symbol.toStringTag] = 'jifasnif'; + t.equal(Object.prototype.toString.call(o2), '[object jifasnif]', 'o2: modified Symbol.toStringTag works'); + + t.deepEqualTest(o1, o2, 'two normal empty objects with different toStringTags', false, false); + + t.end(); +}); From a159a3a0660a2da9a8ff6392aba4b2de81bc8391 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 6 Aug 2019 13:27:55 -0700 Subject: [PATCH 6/6] [Breaking] boxed primitives are unwrapped for comparisons --- index.js | 12 +++++++++++- package.json | 2 ++ test/cmp.js | 24 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 35dcdb3..ff30525 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,8 @@ var isRegex = require('is-regex'); var flags = require('regexp.prototype.flags'); var isArray = require('isarray'); var isDate = require('is-date-object'); +var isBoxedPrimitive = require('is-boxed-primitive'); +var toPrimitive = require('es-to-primitive/es2015'); // TODO: replace this with ES2020 once updated var getTime = Date.prototype.getTime; var gPO = Object.getPrototypeOf; @@ -18,6 +20,12 @@ function deepEqual(actual, expected, options) { return true; } + var actualBoxed = isBoxedPrimitive(actual); + var expectedBoxed = isBoxedPrimitive(expected); + if (actualBoxed || expectedBoxed) { + return deepEqual(toPrimitive(actual), toPrimitive(expected), opts); + } + // 7.3. Other pairs that do not both pass typeof value == 'object', equivalence is determined by ==. if (!actual || !expected || (typeof actual !== 'object' && typeof expected !== 'object')) { return opts.strict ? is(actual, expected) : actual == expected; @@ -53,8 +61,9 @@ function isBuffer(x) { } function objEquiv(a, b, opts) { - /* eslint max-statements: [2, 70] */ + /* eslint max-statements: [2, 70], max-lines-per-function: [2, 80] */ var i, key; + if (typeof a !== typeof b) { return false; } if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { return false; } @@ -121,6 +130,7 @@ function objEquiv(a, b, opts) { for (i = ka.length - 1; i >= 0; i--) { if (ka[i] != kb[i]) { return false; } } + // equivalent values for every corresponding key, and ~~~possibly expensive deep test for (i = ka.length - 1; i >= 0; i--) { key = ka[i]; diff --git a/package.json b/package.json index 8055cc1..89dbe23 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "test": "npm run tests-only" }, "dependencies": { + "es-to-primitive": "^1.2.0", "is-arguments": "^1.0.4", + "is-boxed-primitive": "^1.0.0", "is-date-object": "^1.0.1", "is-regex": "^1.0.4", "isarray": "^2.0.5", diff --git a/test/cmp.js b/test/cmp.js index 49ad67a..4d6871f 100644 --- a/test/cmp.js +++ b/test/cmp.js @@ -419,3 +419,27 @@ test('toStringTag', { skip: !hasSymbols || !Symbol.toStringTag }, function (t) { t.end(); }); + +test('boxed primitives', function (t) { + t.deepEqualTest(Object(false), false, 'boxed and primitive `false`', true, true); + t.deepEqualTest(Object(true), true, 'boxed and primitive `true`', true, true); + t.deepEqualTest(Object(3), 3, 'boxed and primitive `3`', true, true); + t.deepEqualTest(Object(NaN), NaN, 'boxed and primitive `NaN`', false, true); + t.deepEqualTest(Object(''), '', 'boxed and primitive `""`', true, true); + t.deepEqualTest(Object('str'), 'str', 'boxed and primitive `"str"`', true, true); + + t.test('symbol', { skip: !hasSymbols }, function (st) { + var s = Symbol(''); + st.deepEqualTest(Object(s), s, 'boxed and primitive `Symbol()`', true, true); + st.end(); + }); + + /* globals BigInt: false */ + t.test('bigint', { skip: typeof BigInt !== 'function' }, function (st) { + var hhgtg = BigInt(42); + st.deepEqualTest(Object(hhgtg), hhgtg, 'boxed and primitive `BigInt(42)`', true, true); + st.end(); + }); + + t.end(); +});