Skip to content

Commit

Permalink
[New] compare .toString, .valueOf, and [Symbol.toPrimitive] com…
Browse files Browse the repository at this point in the history
…pletions

Co-authored-by: Andrew Rasmussen <[email protected]>
Co-authored-by: Jordan Harband <[email protected]>
  • Loading branch information
arasmussen and ljharb committed Sep 27, 2023
1 parent 8ce483c commit fa8d0ae
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 1 deletion.
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"id-length": [2, { "min": 1, "max": 23 }],
"max-depth": [2, 5],
"max-len": 0,
"max-lines-per-function": [2, { "max": 250 }],
"max-lines-per-function": [2, { "max": 300 }],
"max-params": 0,
"max-statements": [1, 10],
"max-statements-per-line": [2, { "max": 2 }],
"new-cap": [2, { "capIsNewExceptions": ["BigInt"] }],
Expand All @@ -28,6 +29,7 @@
"files": "test/**",
"rules": {
"func-name-matching": 0,
"no-throw-literal": 0,
"prefer-regex-literals": 0,
},
},
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
],
"dependencies": {
"es-get-iterator": "^1.1.3",
"es-to-primitive": "^1.2.1",
"functions-have-names": "^1.2.3",
"has": "^1.0.3",
"has-bigints": "^1.0.2",
Expand Down
56 changes: 56 additions & 0 deletions test/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,62 @@ test('bigints', { skip: !hasBigInts }, function (t) {
t.end();
});

test('toPrimitive', function (t) {
t.test('gracefully handles error throwing', function (mt) {
var toStringThrow = {
toString: function () { throw new Error(); }
};
var valueOfThrow = {
valueOf: function () { throw new Error(); }
};
var noThrow = {};

mt.equal(isEqual(toStringThrow, noThrow), false, 'first argument toPrimitive throws, second does not');
mt.equal(isEqual(noThrow, toStringThrow), false, 'second argument toPrimitive throws, first does not');
mt.equal(isEqual(valueOfThrow, noThrow), false, 'first argument toPrimitive throws, second does not');
mt.equal(isEqual(noThrow, valueOfThrow), false, 'second argument toPrimitive throws; first does not');

mt.end();
});

t.test('toPrimitive strings', function (mt) {
var foo1 = {
toString: function () { return 'foo'; }
};
var foo2 = {
toString: function () { return 'foo'; }
};
var bar = {
toString: function () { return 'bar'; }
};

mt.equal(isEqual(foo1, foo2), true, 'both argument toPrimitive values are equal');
mt.equal(isEqual(foo1, bar), false, 'argument toPrimitive values are not equal');
mt.equal(isEqual(bar, foo1), false, 'argument toPrimitive values are not equal');

mt.end();
});

t.test('toPrimitive numbers', function (mt) {
var value1 = {
valueOf: function () { return 1; }
};
var alsoValue1 = {
valueOf: function () { return 1; }
};
var value2 = {
valueOf: function () { return 2; }
};

mt.equal(isEqual(value1, alsoValue1), true, 'both argument toPrimitive values are equal');
mt.equal(isEqual(value1, value2), false, 'argument toPrimitive values are not equal');
mt.equal(isEqual(value2, value1), false, 'argument toPrimitive values are not equal');

mt.end();
});
t.end();
});

var genericIterator = function (obj) {
var entries = objectEntries(obj);
return function iterator() {
Expand Down
106 changes: 106 additions & 0 deletions test/why.js
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,112 @@ test('bigints', { skip: !hasBigInts }, function (t) {
t.end();
});

test('toPrimitive', function (t) {
t.test('gracefully handles error throwing', function (mt) {
var toStringThrow = {
toString: function () { throw new Error(); }
};
var toStringThrowFalsy = {
toString: function () { throw false; }
};
var toStringThrowFalsy2 = {
toString: function () { throw false; }
};
var valueOfThrow = {
valueOf: function () { throw new Error(); }
};
var noThrow = {};

mt.equal(
isEqualWhy(toStringThrow, toStringThrowFalsy),
'value at key "toString" differs: Function string representations differ',
'both throw; falls back to toString comparison'
);
mt.equal(
isEqualWhy(toStringThrowFalsy, toStringThrowFalsy2),
'',
'both throw; falls back to toString comparison'
);

mt.equal(isEqualWhy(toStringThrow, noThrow), 'first argument toPrimitive (hint String) throws; second does not');
mt.equal(isEqualWhy(noThrow, toStringThrow), 'second argument toPrimitive (hint String) throws; first does not');
mt.equal(isEqualWhy(valueOfThrow, noThrow), 'first argument toPrimitive (hint Number) throws; second does not');
mt.equal(isEqualWhy(noThrow, valueOfThrow), 'second argument toPrimitive (hint Number) throws; first does not');

mt.end();
});

t.test('toPrimitive strings', function (mt) {
var foo1 = {
toString: function () { return 'foo'; }
};
var foo2 = {
toString: function () { return 'foo'; }
};
var bar = {
toString: function () { return 'bar'; }
};

mt.equal(isEqualWhy(foo1, foo2), '');
mt.equal(isEqualWhy(foo1, bar), 'first argument toPrimitive does not match second argument toPrimitive (hint String)');
mt.equal(isEqualWhy(bar, foo1), 'first argument toPrimitive does not match second argument toPrimitive (hint String)');

mt.end();
});

t.test('toPrimitive numbers', function (mt) {
var value1 = {
valueOf: function () { return 1; }
};
var alsoValue1 = {
valueOf: function () { return 1; }
};
var value2 = {
valueOf: function () { return 2; }
};

mt.equal(isEqualWhy(value1, alsoValue1), '');
mt.equal(isEqualWhy(value1, value2), 'first argument toPrimitive does not match second argument toPrimitive (hint Number)');
mt.equal(isEqualWhy(value2, value1), 'first argument toPrimitive does not match second argument toPrimitive (hint Number)');

mt.end();
});

t.test('Symbol.toPrimitive', { skip: !hasSymbols || !Symbol.toPrimitive }, function (st) {
var hints = [];
var obj = {
toString: function () { return 'toString'; },
valueOf: function () { return 'valueOf'; }
};
var obj2 = {
toString: function () { return 'toString'; },
valueOf: function () { return 'valueOf'; }
};
obj2[Symbol.toPrimitive] = function (hint) {
hints.push(hint);
if (hint === 'string') {
return 'toString';
}
if (hint === 'number') {
return 'valueOf';
}
return 'default';
};

st.equal(
isEqualWhy(obj, obj2),
'first argument toPrimitive does not match second argument toPrimitive (hint default)',
'test Symbol.toPrimitive'
);

st.deepEqual(hints, ['string', 'number', 'default'], 'all hints are tested');

st.end();
});

t.end();
});

var genericIterator = function (obj) {
var entries = objectEntries(obj);
return function iterator() {
Expand Down
39 changes: 39 additions & 0 deletions why.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var isSymbol = require('is-symbol');
var isCallable = require('is-callable');
var isBigInt = require('is-bigint');
var getIterator = require('es-get-iterator');
var toPrimitive = require('es-to-primitive/es2015');
var whichCollection = require('which-collection');
var whichBoxedPrimitive = require('which-boxed-primitive');
var getPrototypeOf = require('object.getprototypeof/polyfill')();
Expand All @@ -37,6 +38,32 @@ var normalizeFnWhitespace = function normalizeWhitespace(fnStr) {
return fnStr.replace(/^function ?\(/, 'function (').replace('){', ') {');
};

var testToPrim = function testToPrimitive(value, other, hint, hintName) {
var valPrimitive = NaN;
var valPrimitiveThrows = false;
try {
valPrimitive = toPrimitive(value, hint);
} catch (error) {
valPrimitiveThrows = true;
}

var otherPrimitive = NaN;
var otherPrimitiveThrows = false;
try {
otherPrimitive = toPrimitive(other, hint);
} catch (error) {
otherPrimitiveThrows = true;
}

if (valPrimitiveThrows || otherPrimitiveThrows) {
if (!valPrimitiveThrows) { return 'second argument toPrimitive (hint ' + hintName + ') throws; first does not'; }
if (!otherPrimitiveThrows) { return 'first argument toPrimitive (hint ' + hintName + ') throws; second does not'; }
} else if (valPrimitive !== otherPrimitive) {
return 'first argument toPrimitive does not match second argument toPrimitive (hint ' + hintName + ')';
}
return '';
};

module.exports = function whyNotEqual(value, other) {
if (value === other) { return ''; }
if (value == null || other == null) {
Expand Down Expand Up @@ -205,6 +232,18 @@ module.exports = function whyNotEqual(value, other) {
if (isProto.call(other, value)) { return 'second argument is the [[Prototype]] of the first'; }
if (getPrototypeOf(value) !== getPrototypeOf(other)) { return 'arguments have a different [[Prototype]]'; }

var valueIsFn = typeof value === 'function';
var otherIsFn = typeof other === 'function';

if (!valueIsFn || !otherIsFn) {
var result = testToPrim(value, other, String, 'String')
|| testToPrim(value, other, Number, 'Number')
|| testToPrim(value, other, void undefined, 'default');
if (result) {
return result;
}
}

var valueIterator = getIterator(value);
var otherIterator = getIterator(other);
if (!!valueIterator !== !!otherIterator) {
Expand Down

0 comments on commit fa8d0ae

Please sign in to comment.