Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/bundle-size/find.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ describe('find bundle size', () => {

it('es-toolkit/compat', async () => {
const bundleSize = await getBundleSize('es-toolkit/compat', 'find');
expect(bundleSize).toMatchInlineSnapshot(`8708`);
expect(bundleSize).toMatchInlineSnapshot(`8772`);
});
});
2 changes: 1 addition & 1 deletion benchmarks/bundle-size/findKey.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ describe('findKey bundle size', () => {

it('es-toolkit/compat', async () => {
const bundleSize = await getBundleSize('es-toolkit/compat', 'findKey');
expect(bundleSize).toMatchInlineSnapshot(`8373`);
expect(bundleSize).toMatchInlineSnapshot(`8437`);
});
});
2 changes: 1 addition & 1 deletion benchmarks/bundle-size/mapKeys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ describe('mapKeys bundle size', () => {

it('es-toolkit/compat', async () => {
const bundleSize = await getBundleSize('es-toolkit/compat', 'mapKeys');
expect(bundleSize).toMatchInlineSnapshot(`8386`);
expect(bundleSize).toMatchInlineSnapshot(`8450`);
});
});
2 changes: 1 addition & 1 deletion benchmarks/bundle-size/mapValues.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ describe('mapValues bundle size', () => {

it('es-toolkit/compat', async () => {
const bundleSize = await getBundleSize('es-toolkit/compat', 'mapValues');
expect(bundleSize).toMatchInlineSnapshot(`8386`);
expect(bundleSize).toMatchInlineSnapshot(`8450`);
});
});
41 changes: 41 additions & 0 deletions src/compat/predicate/isMatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

const Foo = function Foo(this: Foo) {
this.a = 1;
} as any as FooConstructor;

Check warning on line 66 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Foo.prototype.b = 2;

Expand All @@ -83,7 +83,7 @@

const Foo = function Foo(this: Foo) {
this.a = 1;
} as any as FooConstructor;

Check warning on line 86 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Foo.prototype.b = 2;

Expand Down Expand Up @@ -153,7 +153,7 @@

const Foo = function Foo(this: Foo, object: Partial<Foo>) {
Object.assign(this, object);
} as any as FooConstructor;

Check warning on line 156 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

// eslint-disable-next-line
// @ts-ignore
Expand Down Expand Up @@ -288,16 +288,16 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch(1, { b: undefined })).toBe(true);
} catch (e: any) {

Check warning on line 291 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
expect(false, e.message);

Check warning on line 292 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Expect must have a corresponding matcher call
}

try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch(1, { a: 1, b: undefined })).toBe(true);
} catch (e: any) {

Check warning on line 299 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
expect(false, e.message);

Check warning on line 300 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Expect must have a corresponding matcher call
}

// eslint-disable-next-line
Expand All @@ -307,8 +307,8 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch(1, { a: { c: undefined } })).toBe(true);
} catch (e: any) {

Check warning on line 310 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
expect(false, e.message);

Check warning on line 311 in src/compat/predicate/isMatch.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Expect must have a corresponding matcher call
}

// eslint-disable-next-line
Expand Down Expand Up @@ -387,4 +387,45 @@
)
).toBe(false);
});

it('should not match nested empty object patterns against non-object targets', () => {
expect(isMatch({ value: 'bar' }, { value: {} })).toBe(false);
expect(isMatch({ value: 123 }, { value: {} })).toBe(false);
expect(isMatch({ value: true }, { value: {} })).toBe(false);
expect(isMatch({ value: null }, { value: {} })).toBe(false);
expect(isMatch({ value: undefined }, { value: {} })).toBe(false);
expect(isMatch({ value: [] }, { value: {} })).toBe(false);
expect(isMatch({ value: new Date(0) }, { value: {} })).toBe(false);
expect(isMatch({ value: { b: 1 } }, { value: {} })).toBe(true);
});

it('should not match nested object patterns against non-plain-object targets', () => {
expect(isMatch({ value: [1, 2, 3] }, { value: { 0: 1 } })).toBe(false);
expect(isMatch({ value: /a/ }, { value: { source: 'a' } })).toBe(false);
expect(isMatch({ value: new String('bar') }, { value: { length: 3 } })).toBe(false);
});

it('should match object patterns against primitive targets at the top level', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch('bar', {})).toBe(true);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch('bar', { length: 3 })).toBe(true);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch('bar', { 0: 'b' })).toBe(true);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch('bar', { length: 4 })).toBe(false);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch('bar', { anyKey: undefined })).toBe(false);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch(123, { anyKey: undefined })).toBe(false);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(isMatch(true, { anyKey: undefined })).toBe(false);
});
});
6 changes: 6 additions & 0 deletions src/compat/predicate/isMatchWith.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
describe('isMatchWith', () => {
it('should provide correct `customizer` arguments', () => {
const argsList: unknown[] = [];
const object1: any = { a: [1, 2], b: null };

Check warning on line 14 in src/compat/predicate/isMatchWith.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const object2: any = { a: [1, 2], b: null };

object1.b = object2;
Expand Down Expand Up @@ -341,6 +341,12 @@
expect(isMatchWith({ a: { b: 2 } }, { a: 1 }, () => undefined)).toBe(false);
});

it('should not match nested empty object patterns against non-object targets', () => {
expect(isMatchWith({ value: 'bar' }, { value: {} }, () => undefined)).toBe(false);
expect(isMatchWith({ value: [] }, { value: {} }, () => undefined)).toBe(false);
expect(isMatchWith({ value: { b: 1 } }, { value: {} }, () => undefined)).toBe(true);
});

it('should handle empty collections', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
Expand Down
29 changes: 23 additions & 6 deletions src/compat/predicate/isMatchWith.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { isObject } from './isObject.ts';
import { isPrimitive } from '../../predicate/isPrimitive.ts';
import { getTag } from '../_internal/getTag.ts';
import type { IsMatchWithCustomizer } from '../_internal/IsMatchWithCustomizer.ts';
import { argumentsTag, objectTag } from '../_internal/tags.ts';
import { eq } from '../util/eq.ts';

/**
Expand Down Expand Up @@ -46,7 +48,8 @@ export function isMatchWith(target: object, source: object, compare: IsMatchWith
* - **Primitives**: Matches using strict equality
*
* Special cases:
* - Empty objects, arrays, Maps, and Sets always match any target
* - Empty arrays, Maps, and Sets always match any target
* - An empty object matches any target at the top level, but at nested levels it only matches object targets
* - `null` and `undefined` source values have specific matching rules
* - Circular references are handled using an internal stack to prevent infinite recursion
*
Expand Down Expand Up @@ -151,7 +154,7 @@ function isMatchWithInternal(

switch (typeof source) {
case 'object': {
return isObjectMatch(target, source, compare, stack);
return isObjectMatch(target, source, compare, stack, isRoot);
}
case 'function': {
const sourceKeys = Object.keys(source);
Expand Down Expand Up @@ -191,7 +194,8 @@ function isObjectMatch(
source: any,
stack?: Map<any, any>
) => boolean | undefined,
stack: Map<any, any> | undefined
stack: Map<any, any> | undefined,
isRoot = false
): boolean {
if (source == null) {
return true;
Expand All @@ -211,8 +215,21 @@ function isObjectMatch(

const keys = Object.keys(source as any);

if (target == null || isPrimitive(target)) {
return keys.length === 0;
if (target == null) {
return isRoot && keys.length === 0;
}

if (isRoot) {
if (isPrimitive(target)) {
target = Object(target);
}
} else {
// Nested object patterns only match plain object-like targets, like lodash's tag comparison.
const tag = getTag(target);

if (tag !== objectTag && tag !== argumentsTag) {
return false;
}
}

if (keys.length === 0) {
Expand All @@ -229,7 +246,7 @@ function isObjectMatch(
for (let i = 0; i < keys.length; i++) {
const key = keys[i];

if (!isPrimitive(target) && !(key in target)) {
if (!(key in target)) {
return false;
}

Expand Down
5 changes: 5 additions & 0 deletions src/compat/predicate/matches.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ describe('matches', () => {
expect(matches({ a: 1 })({ a: { b: 2 } })).toBe(false);
});

it('should not match nested empty object patterns against non-object targets', () => {
expect(matches({ value: {} })({ value: 'bar' })).toBe(false);
expect(matches({ value: {} })({ value: { b: 1 } })).toBe(true);
});

it(`should match inherited string keyed \`object\` properties`, () => {
interface Foo {
a: number;
Expand Down