mirror of
https://github.com/denoland/std.git
synced 2024-11-21 20:50:22 +00:00
fix(assert): check property equality up the prototype chain (#6153)
This commit is contained in:
parent
bc20dbe21b
commit
63fdf8d090
174
assert/equal.ts
174
assert/equal.ts
@ -6,17 +6,92 @@ function isKeyedCollection(x: unknown): x is KeyedCollection {
|
|||||||
return x instanceof Set || x instanceof Map;
|
return x instanceof Set || x instanceof Map;
|
||||||
}
|
}
|
||||||
|
|
||||||
function constructorsEqual(a: object, b: object) {
|
function prototypesEqual(a: object, b: object) {
|
||||||
return a.constructor === b.constructor ||
|
const pa = Object.getPrototypeOf(a);
|
||||||
a.constructor === Object && !b.constructor ||
|
const pb = Object.getPrototypeOf(b);
|
||||||
!a.constructor && b.constructor === Object;
|
return pa === pb ||
|
||||||
|
pa === Object.prototype && pb === null ||
|
||||||
|
pa === null && pb === Object.prototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBasicObjectOrArray(obj: object) {
|
||||||
|
const proto = Object.getPrototypeOf(obj);
|
||||||
|
return proto === null || proto === Object.prototype ||
|
||||||
|
proto === Array.prototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slightly faster than Reflect.ownKeys in V8 as of 12.9.202.13-rusty (2024-10-28)
|
||||||
|
function ownKeys(obj: object) {
|
||||||
|
return [
|
||||||
|
...Object.getOwnPropertyNames(obj),
|
||||||
|
...Object.getOwnPropertySymbols(obj),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeysDeep(obj: object) {
|
||||||
|
const keys = new Set<string | symbol>();
|
||||||
|
|
||||||
|
while (obj !== Object.prototype && obj !== Array.prototype && obj != null) {
|
||||||
|
for (const key of ownKeys(obj)) {
|
||||||
|
keys.add(key);
|
||||||
|
}
|
||||||
|
obj = Object.getPrototypeOf(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
const Temporal: any = (globalThis as any).Temporal ??
|
||||||
|
new Proxy({}, { get: () => {} });
|
||||||
|
|
||||||
|
/** A non-exhaustive list of prototypes that can be accurately fast-path compared with `String(instance)` */
|
||||||
|
const stringComparablePrototypes = new Set<unknown>(
|
||||||
|
[
|
||||||
|
Intl.Locale,
|
||||||
|
RegExp,
|
||||||
|
Temporal.Duration,
|
||||||
|
Temporal.Instant,
|
||||||
|
Temporal.PlainDate,
|
||||||
|
Temporal.PlainDateTime,
|
||||||
|
Temporal.PlainTime,
|
||||||
|
Temporal.PlainYearMonth,
|
||||||
|
Temporal.PlainMonthDay,
|
||||||
|
Temporal.ZonedDateTime,
|
||||||
|
URL,
|
||||||
|
URLSearchParams,
|
||||||
|
].filter((x) => x != null).map((x) => x.prototype),
|
||||||
|
);
|
||||||
|
|
||||||
|
function isPrimitive(x: unknown) {
|
||||||
|
return typeof x === "string" ||
|
||||||
|
typeof x === "number" ||
|
||||||
|
typeof x === "boolean" ||
|
||||||
|
typeof x === "bigint" ||
|
||||||
|
typeof x === "symbol" ||
|
||||||
|
x == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypedArray = Pick<Uint8Array | BigUint64Array, "length" | number>;
|
||||||
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
||||||
|
function compareTypedArrays(a: TypedArray, b: TypedArray) {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
for (let i = 0; i < b.length; i++) {
|
||||||
|
if (!sameValueZero(a[i], b[i])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check both strict equality (`0 == -0`) and `Object.is` (`NaN == NaN`) */
|
||||||
|
function sameValueZero(a: unknown, b: unknown) {
|
||||||
|
return a === b || Object.is(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep equality comparison used in assertions.
|
* Deep equality comparison used in assertions.
|
||||||
*
|
*
|
||||||
* @param c The actual value
|
* @param a The actual value
|
||||||
* @param d The expected value
|
* @param b The expected value
|
||||||
* @returns `true` if the values are deeply equal, `false` otherwise
|
* @returns `true` if the values are deeply equal, `false` otherwise
|
||||||
*
|
*
|
||||||
* @example Usage
|
* @example Usage
|
||||||
@ -24,53 +99,33 @@ function constructorsEqual(a: object, b: object) {
|
|||||||
* import { equal } from "@std/assert/equal";
|
* import { equal } from "@std/assert/equal";
|
||||||
*
|
*
|
||||||
* equal({ foo: "bar" }, { foo: "bar" }); // Returns `true`
|
* equal({ foo: "bar" }, { foo: "bar" }); // Returns `true`
|
||||||
* equal({ foo: "bar" }, { foo: "baz" }); // Returns `false
|
* equal({ foo: "bar" }, { foo: "baz" }); // Returns `false`
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function equal(c: unknown, d: unknown): boolean {
|
export function equal(a: unknown, b: unknown): boolean {
|
||||||
const seen = new Map();
|
const seen = new Map<unknown, unknown>();
|
||||||
return (function compare(a: unknown, b: unknown): boolean {
|
return (function compare(a: unknown, b: unknown): boolean {
|
||||||
// Have to render RegExp & Date for string comparison
|
if (sameValueZero(a, b)) return true;
|
||||||
// unless it's mistreated as object
|
if (isPrimitive(a) || isPrimitive(b)) return false;
|
||||||
if (
|
|
||||||
a &&
|
|
||||||
b &&
|
|
||||||
((a instanceof RegExp && b instanceof RegExp) ||
|
|
||||||
(a instanceof URL && b instanceof URL))
|
|
||||||
) {
|
|
||||||
return String(a) === String(b);
|
|
||||||
}
|
|
||||||
if (a instanceof Date && b instanceof Date) {
|
if (a instanceof Date && b instanceof Date) {
|
||||||
const aTime = a.getTime();
|
return Object.is(a.getTime(), b.getTime());
|
||||||
const bTime = b.getTime();
|
|
||||||
// Check for NaN equality manually since NaN is not
|
|
||||||
// equal to itself.
|
|
||||||
if (Number.isNaN(aTime) && Number.isNaN(bTime)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return aTime === bTime;
|
|
||||||
}
|
|
||||||
if (typeof a === "number" && typeof b === "number") {
|
|
||||||
return Number.isNaN(a) && Number.isNaN(b) || a === b;
|
|
||||||
}
|
|
||||||
if (Object.is(a, b)) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if (a && typeof a === "object" && b && typeof b === "object") {
|
if (a && typeof a === "object" && b && typeof b === "object") {
|
||||||
if (a && b && !constructorsEqual(a, b)) {
|
if (!prototypesEqual(a, b)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (a instanceof WeakMap || b instanceof WeakMap) {
|
if (a instanceof TypedArray) {
|
||||||
if (!(a instanceof WeakMap && b instanceof WeakMap)) return false;
|
return compareTypedArrays(a as TypedArray, b as TypedArray);
|
||||||
|
}
|
||||||
|
if (a instanceof WeakMap) {
|
||||||
throw new TypeError("cannot compare WeakMap instances");
|
throw new TypeError("cannot compare WeakMap instances");
|
||||||
}
|
}
|
||||||
if (a instanceof WeakSet || b instanceof WeakSet) {
|
if (a instanceof WeakSet) {
|
||||||
if (!(a instanceof WeakSet && b instanceof WeakSet)) return false;
|
|
||||||
throw new TypeError("cannot compare WeakSet instances");
|
throw new TypeError("cannot compare WeakSet instances");
|
||||||
}
|
}
|
||||||
if (a instanceof WeakRef || b instanceof WeakRef) {
|
if (a instanceof WeakRef) {
|
||||||
if (!(a instanceof WeakRef && b instanceof WeakRef)) return false;
|
return compare(a.deref(), (b as WeakRef<WeakKey>).deref());
|
||||||
return compare(a.deref(), b.deref());
|
|
||||||
}
|
}
|
||||||
if (seen.get(a) === b) {
|
if (seen.get(a) === b) {
|
||||||
return true;
|
return true;
|
||||||
@ -85,14 +140,7 @@ export function equal(c: unknown, d: unknown): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const aKeys = [...a.keys()];
|
const aKeys = [...a.keys()];
|
||||||
const primitiveKeysFastPath = aKeys.every((k) => {
|
const primitiveKeysFastPath = aKeys.every(isPrimitive);
|
||||||
return typeof k === "string" ||
|
|
||||||
typeof k === "number" ||
|
|
||||||
typeof k === "boolean" ||
|
|
||||||
typeof k === "bigint" ||
|
|
||||||
typeof k === "symbol" ||
|
|
||||||
k == null;
|
|
||||||
});
|
|
||||||
if (primitiveKeysFastPath) {
|
if (primitiveKeysFastPath) {
|
||||||
if (a instanceof Set) {
|
if (a instanceof Set) {
|
||||||
return a.symmetricDifference(b).size === 0;
|
return a.symmetricDifference(b).size === 0;
|
||||||
@ -130,15 +178,23 @@ export function equal(c: unknown, d: unknown): boolean {
|
|||||||
|
|
||||||
return unmatchedEntries === 0;
|
return unmatchedEntries === 0;
|
||||||
}
|
}
|
||||||
const merged = { ...a, ...b };
|
|
||||||
for (
|
let keys: Iterable<string | symbol>;
|
||||||
const key of [
|
|
||||||
...Object.getOwnPropertyNames(merged),
|
if (isBasicObjectOrArray(a)) {
|
||||||
...Object.getOwnPropertySymbols(merged),
|
// fast path
|
||||||
]
|
keys = ownKeys({ ...a, ...b });
|
||||||
) {
|
} else if (stringComparablePrototypes.has(Object.getPrototypeOf(a))) {
|
||||||
type Key = keyof typeof merged;
|
// medium path
|
||||||
if (!compare(a && a[key as Key], b && b[key as Key])) {
|
return String(a) === String(b);
|
||||||
|
} else {
|
||||||
|
// slow path
|
||||||
|
keys = getKeysDeep(a).union(getKeysDeep(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
type Key = keyof typeof a;
|
||||||
|
if (!compare(a[key as Key], b[key as Key])) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) {
|
if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) {
|
||||||
@ -148,5 +204,5 @@ export function equal(c: unknown, d: unknown): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
})(c, d);
|
})(a, b);
|
||||||
}
|
}
|
||||||
|
@ -344,3 +344,149 @@ Deno.test("equal() keyed collection edge cases", () => {
|
|||||||
new Map([[1, { a: 1 }], [2, { b: 3 }]]),
|
new Map([[1, { a: 1 }], [2, { b: 3 }]]),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("equal() with constructor and prototype", async (t) => {
|
||||||
|
await t.step(
|
||||||
|
"value-equal but reference-unequal property named `constructor`",
|
||||||
|
() => {
|
||||||
|
assert(equal(
|
||||||
|
{ constructor: { x: 1 } },
|
||||||
|
{ constructor: { x: 1 } },
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await t.step(
|
||||||
|
"instance vs plain object with property named `constructor`",
|
||||||
|
() => {
|
||||||
|
class X {}
|
||||||
|
const a = new X();
|
||||||
|
const b = { constructor: X };
|
||||||
|
assert(equal(a.constructor, b.constructor));
|
||||||
|
assertFalse(equal(a, b));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await t.step("manually set prototype", () => {
|
||||||
|
class X {
|
||||||
|
prop = 1;
|
||||||
|
}
|
||||||
|
const a = new X();
|
||||||
|
const b = {} as X;
|
||||||
|
|
||||||
|
// false as prototype differs
|
||||||
|
assertFalse(equal(a, b));
|
||||||
|
Object.setPrototypeOf(b, X.prototype);
|
||||||
|
// still false as `prop` is not set
|
||||||
|
assertFalse(equal(a, b));
|
||||||
|
b.prop = 1;
|
||||||
|
// now true
|
||||||
|
assert(equal(a, b));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("equal() with dynamic properties defined on the prototype", async (t) => {
|
||||||
|
await t.step("built-in web APIs", async (t) => {
|
||||||
|
await t.step("URLPattern", () => {
|
||||||
|
assert(equal(
|
||||||
|
new URLPattern("*://*.*"),
|
||||||
|
new URLPattern("*://*.*"),
|
||||||
|
));
|
||||||
|
|
||||||
|
assertFalse(equal(
|
||||||
|
new URLPattern("*://*.*"),
|
||||||
|
new URLPattern("*://*.com"),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step("URLSearchParams", () => {
|
||||||
|
assert(equal(
|
||||||
|
new URLSearchParams("a=1&b=2"),
|
||||||
|
new URLSearchParams("a=1&b=2"),
|
||||||
|
));
|
||||||
|
|
||||||
|
assertFalse(equal(
|
||||||
|
new URLSearchParams("a=1&b=2"),
|
||||||
|
new URLSearchParams("a=1&b=99999"),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step("Intl.Locale", () => {
|
||||||
|
assert(equal(
|
||||||
|
new Intl.Locale("es-MX"),
|
||||||
|
new Intl.Locale("es-MX"),
|
||||||
|
));
|
||||||
|
|
||||||
|
assertFalse(equal(
|
||||||
|
new Intl.Locale("es-MX"),
|
||||||
|
new Intl.Locale("pt-BR"),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step("custom prototype", () => {
|
||||||
|
type Obj = {
|
||||||
|
prop: number;
|
||||||
|
};
|
||||||
|
const wm = new WeakMap<Obj, number>();
|
||||||
|
const proto: Obj = {
|
||||||
|
get prop() {
|
||||||
|
return wm.get(this)!;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const makeObj = (val: number): Obj => {
|
||||||
|
const x = Object.create(proto);
|
||||||
|
wm.set(x, val);
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert(equal(
|
||||||
|
makeObj(1),
|
||||||
|
makeObj(1),
|
||||||
|
));
|
||||||
|
|
||||||
|
assertFalse(equal(
|
||||||
|
makeObj(1),
|
||||||
|
makeObj(99999),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("equal() with typed arrays", async (t) => {
|
||||||
|
await t.step("Uint8Array", async (t) => {
|
||||||
|
await t.step("equal", () => {
|
||||||
|
assert(equal(
|
||||||
|
new Uint8Array([1, 2, 3]),
|
||||||
|
new Uint8Array([1, 2, 3]),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step("unequal", () => {
|
||||||
|
assertFalse(equal(
|
||||||
|
new Uint8Array([1, 2, 3]),
|
||||||
|
new Uint8Array([1, 2, 4]),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step("length unequal", () => {
|
||||||
|
assertFalse(equal(
|
||||||
|
new Uint8Array([0]),
|
||||||
|
new Uint8Array([0, 0]),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step("Float64Array", async (t) => {
|
||||||
|
await t.step("NaN == NaN", () => {
|
||||||
|
assert(equal(
|
||||||
|
new Float64Array([NaN]),
|
||||||
|
new Float64Array([NaN]),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step("0 == -0", () => {
|
||||||
|
assert(equal(
|
||||||
|
new Float64Array([0]),
|
||||||
|
new Float64Array([-0]),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user