mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
assert,util: fix constructor lookup in deep equal comparison
The latest performance optimization did not take into account that an object may have a property called constructor. This is addressed in this PR by adding a new fast path and using fallbacks. PR-URL: https://github.com/nodejs/node/pull/57876 Reviewed-By: Jordan Harband <ljharb@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
1 parent
2b76dca0fa
commit
733e0fc2c4
4 files changed
+211
-50
No files matched your search
@@ -10,6 +10,7 @@ const notCircular = {};
|
||||
notCircular.circular = {};
|
||||
|
||||
const primValues = {
|
||||
'null_prototype': { __proto__: null },
|
||||
'string': 'abcdef',
|
||||
'number': 1_000,
|
||||
'boolean': true,
|
||||
@@ -24,6 +25,7 @@ const primValues = {
|
||||
};
|
||||
|
||||
const primValues2 = {
|
||||
'null_prototype': { __proto__: null },
|
||||
'object': { property: 'abcdef' },
|
||||
'array': [1, 2, 3],
|
||||
'set_object': new Set([[1]]),
|
||||
@@ -35,6 +37,7 @@ const primValues2 = {
|
||||
};
|
||||
|
||||
const primValuesUnequal = {
|
||||
'null_prototype': { __proto__: { __proto__: null } },
|
||||
'string': 'abcdez',
|
||||
'number': 1_001,
|
||||
'boolean': false,
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
Array,
|
||||
ArrayBuffer,
|
||||
ArrayIsArray,
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypePush,
|
||||
BigInt,
|
||||
BigInt64Array,
|
||||
BigIntPrototypeValueOf,
|
||||
BigUint64Array,
|
||||
Boolean,
|
||||
BooleanPrototypeValueOf,
|
||||
DataView,
|
||||
Date,
|
||||
DatePrototypeGetTime,
|
||||
Error,
|
||||
Float32Array,
|
||||
Float64Array,
|
||||
Function,
|
||||
Int16Array,
|
||||
Int32Array,
|
||||
Int8Array,
|
||||
Map,
|
||||
Number,
|
||||
NumberPrototypeValueOf,
|
||||
Object,
|
||||
ObjectGetOwnPropertyDescriptor,
|
||||
ObjectGetOwnPropertySymbols: getOwnSymbols,
|
||||
ObjectGetPrototypeOf,
|
||||
@@ -17,18 +34,67 @@ const {
|
||||
ObjectPrototypeHasOwnProperty: hasOwn,
|
||||
ObjectPrototypePropertyIsEnumerable: hasEnumerable,
|
||||
ObjectPrototypeToString,
|
||||
Promise,
|
||||
RegExp,
|
||||
SafeSet,
|
||||
Set,
|
||||
String,
|
||||
StringPrototypeValueOf,
|
||||
Symbol,
|
||||
SymbolPrototypeValueOf,
|
||||
TypedArrayPrototypeGetByteLength: getByteLength,
|
||||
TypedArrayPrototypeGetSymbolToStringTag,
|
||||
Uint16Array,
|
||||
Uint32Array,
|
||||
Uint8Array,
|
||||
Uint8ClampedArray,
|
||||
WeakMap,
|
||||
WeakSet,
|
||||
globalThis: { Float16Array },
|
||||
} = primordials;
|
||||
|
||||
const { compare } = internalBinding('buffer');
|
||||
const assert = require('internal/assert');
|
||||
const { isURL } = require('internal/url');
|
||||
const { isError } = require('internal/util');
|
||||
const { Buffer } = require('buffer');
|
||||
|
||||
const wellKnownConstructors = new SafeSet()
|
||||
.add(Array)
|
||||
.add(ArrayBuffer)
|
||||
.add(BigInt)
|
||||
.add(BigInt64Array)
|
||||
.add(BigUint64Array)
|
||||
.add(Boolean)
|
||||
.add(Buffer)
|
||||
.add(DataView)
|
||||
.add(Date)
|
||||
.add(Error)
|
||||
.add(Float32Array)
|
||||
.add(Float64Array)
|
||||
.add(Function)
|
||||
.add(Int16Array)
|
||||
.add(Int32Array)
|
||||
.add(Int8Array)
|
||||
.add(Map)
|
||||
.add(Number)
|
||||
.add(Object)
|
||||
.add(Promise)
|
||||
.add(RegExp)
|
||||
.add(Set)
|
||||
.add(String)
|
||||
.add(Symbol)
|
||||
.add(Uint16Array)
|
||||
.add(Uint32Array)
|
||||
.add(Uint8Array)
|
||||
.add(Uint8ClampedArray)
|
||||
.add(WeakMap)
|
||||
.add(WeakSet);
|
||||
|
||||
if (Float16Array) { // TODO(BridgeAR): Remove when regularly supported
|
||||
wellKnownConstructors.add(Float16Array);
|
||||
}
|
||||
|
||||
const types = require('internal/util/types');
|
||||
const {
|
||||
isAnyArrayBuffer,
|
||||
@@ -198,11 +264,15 @@ function innerDeepEqual(val1, val2, mode, memos) {
|
||||
}
|
||||
|
||||
function objectComparisonStart(val1, val2, mode, memos) {
|
||||
if (mode === kStrict &&
|
||||
(val1.constructor !== val2.constructor ||
|
||||
(val1.constructor === undefined &&
|
||||
ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2)))) {
|
||||
return false;
|
||||
if (mode === kStrict) {
|
||||
if (wellKnownConstructors.has(val1.constructor) ||
|
||||
(val1.constructor !== undefined && !hasOwn(val1, 'constructor'))) {
|
||||
if (val1.constructor !== val2.constructor) {
|
||||
return false;
|
||||
}
|
||||
} else if (ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const val1Tag = ObjectPrototypeToString(val1);
|
||||
|
||||
@@ -20,10 +20,18 @@ const {
|
||||
ArrayPrototypeSplice,
|
||||
ArrayPrototypeUnshift,
|
||||
BigIntPrototypeValueOf,
|
||||
Boolean,
|
||||
BooleanPrototype,
|
||||
BooleanPrototypeValueOf,
|
||||
DataView,
|
||||
DataViewPrototype,
|
||||
Date,
|
||||
DatePrototype,
|
||||
DatePrototypeGetTime,
|
||||
DatePrototypeToISOString,
|
||||
DatePrototypeToString,
|
||||
Error,
|
||||
ErrorPrototype,
|
||||
ErrorPrototypeToString,
|
||||
Function,
|
||||
FunctionPrototype,
|
||||
@@ -47,6 +55,7 @@ const {
|
||||
NumberIsNaN,
|
||||
NumberParseFloat,
|
||||
NumberParseInt,
|
||||
NumberPrototype,
|
||||
NumberPrototypeToString,
|
||||
NumberPrototypeValueOf,
|
||||
Object,
|
||||
@@ -63,9 +72,12 @@ const {
|
||||
ObjectPrototypePropertyIsEnumerable,
|
||||
ObjectSeal,
|
||||
ObjectSetPrototypeOf,
|
||||
Promise,
|
||||
PromisePrototype,
|
||||
ReflectApply,
|
||||
ReflectOwnKeys,
|
||||
RegExp,
|
||||
RegExpPrototype,
|
||||
RegExpPrototypeExec,
|
||||
RegExpPrototypeSymbolReplace,
|
||||
RegExpPrototypeSymbolSplit,
|
||||
@@ -78,6 +90,7 @@ const {
|
||||
SetPrototypeGetSize,
|
||||
SetPrototypeValues,
|
||||
String,
|
||||
StringPrototype,
|
||||
StringPrototypeCharCodeAt,
|
||||
StringPrototypeCodePointAt,
|
||||
StringPrototypeEndsWith,
|
||||
@@ -106,6 +119,10 @@ const {
|
||||
TypedArrayPrototypeGetLength,
|
||||
TypedArrayPrototypeGetSymbolToStringTag,
|
||||
Uint8Array,
|
||||
WeakMap,
|
||||
WeakMapPrototype,
|
||||
WeakSet,
|
||||
WeakSetPrototype,
|
||||
globalThis,
|
||||
uncurryThis,
|
||||
} = primordials;
|
||||
@@ -608,21 +625,31 @@ function isInstanceof(object, proto) {
|
||||
}
|
||||
|
||||
// Special-case for some builtin prototypes in case their `constructor` property has been tampered.
|
||||
const wellKnownPrototypes = new SafeMap();
|
||||
wellKnownPrototypes.set(ArrayPrototype, { name: 'Array', constructor: Array });
|
||||
wellKnownPrototypes.set(ArrayBufferPrototype, { name: 'ArrayBuffer', constructor: ArrayBuffer });
|
||||
wellKnownPrototypes.set(FunctionPrototype, { name: 'Function', constructor: Function });
|
||||
wellKnownPrototypes.set(MapPrototype, { name: 'Map', constructor: Map });
|
||||
wellKnownPrototypes.set(ObjectPrototype, { name: 'Object', constructor: Object });
|
||||
wellKnownPrototypes.set(SetPrototype, { name: 'Set', constructor: Set });
|
||||
wellKnownPrototypes.set(TypedArrayPrototype, { name: 'TypedArray', constructor: TypedArray });
|
||||
const wellKnownPrototypes = new SafeMap()
|
||||
.set(ArrayPrototype, { name: 'Array', constructor: Array })
|
||||
.set(ArrayBufferPrototype, { name: 'ArrayBuffer', constructor: ArrayBuffer })
|
||||
.set(FunctionPrototype, { name: 'Function', constructor: Function })
|
||||
.set(MapPrototype, { name: 'Map', constructor: Map })
|
||||
.set(SetPrototype, { name: 'Set', constructor: Set })
|
||||
.set(ObjectPrototype, { name: 'Object', constructor: Object })
|
||||
.set(TypedArrayPrototype, { name: 'TypedArray', constructor: TypedArray })
|
||||
.set(RegExpPrototype, { name: 'RegExp', constructor: RegExp })
|
||||
.set(DatePrototype, { name: 'Date', constructor: Date })
|
||||
.set(DataViewPrototype, { name: 'DataView', constructor: DataView })
|
||||
.set(ErrorPrototype, { name: 'Error', constructor: Error })
|
||||
.set(BooleanPrototype, { name: 'Boolean', constructor: Boolean })
|
||||
.set(NumberPrototype, { name: 'Number', constructor: Number })
|
||||
.set(StringPrototype, { name: 'String', constructor: String })
|
||||
.set(PromisePrototype, { name: 'Promise', constructor: Promise })
|
||||
.set(WeakMapPrototype, { name: 'WeakMap', constructor: WeakMap })
|
||||
.set(WeakSetPrototype, { name: 'WeakSet', constructor: WeakSet });
|
||||
|
||||
function getConstructorName(obj, ctx, recurseTimes, protoProps) {
|
||||
let firstProto;
|
||||
const tmp = obj;
|
||||
while (obj || isUndetectableObject(obj)) {
|
||||
const wellKnownPrototypeNameAndConstructor = wellKnownPrototypes.get(obj);
|
||||
if (wellKnownPrototypeNameAndConstructor != null) {
|
||||
if (wellKnownPrototypeNameAndConstructor !== undefined) {
|
||||
const { name, constructor } = wellKnownPrototypeNameAndConstructor;
|
||||
if (FunctionPrototypeSymbolHasInstance(constructor, tmp)) {
|
||||
if (protoProps !== undefined && firstProto !== obj) {
|
||||
|
||||
@@ -1527,46 +1527,107 @@ test('Detects differences in deeply nested arrays instead of seeing a new object
|
||||
);
|
||||
});
|
||||
|
||||
// check URL
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://bar');
|
||||
test('URLs', () => {
|
||||
// check URL
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://bar');
|
||||
|
||||
assertNotDeepOrStrict(a, b);
|
||||
}
|
||||
assertNotDeepOrStrict(a, b);
|
||||
}
|
||||
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://foo');
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://foo');
|
||||
|
||||
assertDeepAndStrictEqual(a, b);
|
||||
}
|
||||
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://foo');
|
||||
a.bar = 1;
|
||||
b.bar = 2;
|
||||
assertNotDeepOrStrict(a, b);
|
||||
}
|
||||
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://foo');
|
||||
a.bar = 1;
|
||||
b.bar = 1;
|
||||
assertDeepAndStrictEqual(a, b);
|
||||
}
|
||||
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://bar');
|
||||
assert.throws(
|
||||
() => assert.deepStrictEqual(a, b),
|
||||
{
|
||||
code: 'ERR_ASSERTION',
|
||||
name: 'AssertionError',
|
||||
message: /http:\/\/bar/
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Own property constructor properties should check against the original prototype', () => {
|
||||
const a = { constructor: { name: 'Foo' } };
|
||||
const b = { constructor: { name: 'Foo' } };
|
||||
assertDeepAndStrictEqual(a, b);
|
||||
}
|
||||
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://foo');
|
||||
a.bar = 1;
|
||||
b.bar = 2;
|
||||
assertNotDeepOrStrict(a, b);
|
||||
}
|
||||
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://foo');
|
||||
a.bar = 1;
|
||||
b.bar = 1;
|
||||
let prototype = {};
|
||||
Object.setPrototypeOf(a, prototype);
|
||||
Object.setPrototypeOf(b, prototype);
|
||||
assertDeepAndStrictEqual(a, b);
|
||||
}
|
||||
|
||||
{
|
||||
const a = new URL('http://foo');
|
||||
const b = new URL('http://bar');
|
||||
assert.throws(
|
||||
() => assert.deepStrictEqual(a, b),
|
||||
{
|
||||
code: 'ERR_ASSERTION',
|
||||
name: 'AssertionError',
|
||||
message: /http:\/\/bar/
|
||||
}
|
||||
);
|
||||
}
|
||||
Object.setPrototypeOf(b, {});
|
||||
assertNotDeepOrStrict(a, {});
|
||||
|
||||
prototype = { __proto__: null };
|
||||
Object.setPrototypeOf(a, prototype);
|
||||
Object.setPrototypeOf(b, prototype);
|
||||
assertDeepAndStrictEqual(a, b);
|
||||
|
||||
Object.setPrototypeOf(b, { __proto__: null });
|
||||
assert.notDeepStrictEqual(a, b);
|
||||
assert.notDeepStrictEqual(b, a);
|
||||
|
||||
// Turn off no-restricted-properties because we are testing deepEqual!
|
||||
/* eslint-disable no-restricted-properties */
|
||||
assert.deepEqual(a, b);
|
||||
assert.deepEqual(b, a);
|
||||
});
|
||||
|
||||
test('Inherited null prototype without own constructor properties should check the correct prototype', () => {
|
||||
const a = { foo: { name: 'Foo' } };
|
||||
const b = { foo: { name: 'Foo' } };
|
||||
assertDeepAndStrictEqual(a, b);
|
||||
|
||||
let prototype = {};
|
||||
Object.setPrototypeOf(a, prototype);
|
||||
Object.setPrototypeOf(b, prototype);
|
||||
assertDeepAndStrictEqual(a, b);
|
||||
|
||||
Object.setPrototypeOf(b, {});
|
||||
assertNotDeepOrStrict(a, {});
|
||||
|
||||
prototype = { __proto__: null };
|
||||
Object.setPrototypeOf(a, prototype);
|
||||
Object.setPrototypeOf(b, prototype);
|
||||
assertDeepAndStrictEqual(a, b);
|
||||
|
||||
Object.setPrototypeOf(b, { __proto__: null });
|
||||
assert.notDeepStrictEqual(a, b);
|
||||
assert.notDeepStrictEqual(b, a);
|
||||
|
||||
assert.notDeepStrictEqual({ __proto__: null }, { __proto__: { __proto__: null } });
|
||||
assert.notDeepStrictEqual({ __proto__: { __proto__: null } }, { __proto__: null });
|
||||
|
||||
// Turn off no-restricted-properties because we are testing deepEqual!
|
||||
/* eslint-disable no-restricted-properties */
|
||||
assert.deepEqual(a, b);
|
||||
assert.deepEqual(b, a);
|
||||
});
|
||||
Reference in new issue
Block a user