Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: remove _.get and _.set #54

Closed
wants to merge 2 commits into from
Closed
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
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"unicorn/prefer-string-replace-all": "off",
"unicorn/prevent-abbreviations": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-null": "off",
"unicorn/no-this-assignment": "off",
"unicorn/no-unused-properties": "off",
"unicorn/prefer-module": "off"
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,7 @@
"bunyan": "^2.0.5",
"bunyan-debug-stream": "^3.1.0",
"funpermaproxy": "^1.1.0",
"lodash.get": "^4.4.2",
"lodash.merge": "^4.6.2",
"lodash.set": "^4.3.2",
"node-ipc": "9.2.1",
"strip-ansi": "^6.0.0",
"tslib": "^2.5.3"
Expand Down
6 changes: 2 additions & 4 deletions src/metadata/containers/BaseMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/* eslint-disable unicorn/no-null */
import lodashGet from 'lodash.get';
import lodashMerge from 'lodash.merge';
import lodashSet from 'lodash.set';

import { logger, optimizeTracing } from '../../utils';
import { get as lodashGet, set as lodashSet, logger, optimizeTracing } from '../../utils';
import type { AggregatedIdentifier } from '../ids';
import * as symbols from '../symbols';
import type { Data, Metadata } from '../types';
Expand Down Expand Up @@ -122,7 +120,7 @@ export abstract class BaseMetadata implements Metadata {
);
}

const array = lodashGet(this[symbols.data], path, []);
const array = lodashGet(this[symbols.data], path, [] as unknown[]);
if (!Array.isArray(array)) {
throw new TypeError(
`Cannot ${operation} to path "${path}", because it is not an array, but: ${array}`,
Expand Down
52 changes: 52 additions & 0 deletions src/utils/get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { get } from './get';

describe('get function', () => {
it('retrieves a simple property', () => {
expect(get({ a: 42 }, 'a')).toBe(42);
});

it('retrieves a nested property', () => {
expect(get({ a: { b: { c: 42 } } }, 'a.b.c')).toBe(42);
});

it('retrieves a nested property using array syntax', () => {
expect(get({ a: { b: { c: 42 } } }, ['a', 'b', 'c'])).toBe(42);
});

it('returns undefined for nonexistent properties', () => {
expect(get({}, 'a')).toBeUndefined();
});

it('retrieves a property that is explicitly set to null', () => {
expect(get({ a: null }, 'a')).toBeNull();
});

it('returns undefined for an incomplete path', () => {
expect(get({ a: 42 }, 'a.b')).toBeUndefined();
});

it('returns undefined for a nonexistent nested property', () => {
expect(get({ a: { b: 42 } }, 'a.b.c')).toBeUndefined();
});

it('returns undefined when object is a primitive', () => {
expect(get(42, 'a')).toBeUndefined();
});

it('returns undefined when object is null', () => {
expect(get(null, 'a')).toBeUndefined();
});

it('returns undefined when object is undefined', () => {
expect(get(undefined, 'a')).toBeUndefined();
});

it('returns the default value if provided and property is not found', () => {
expect(get({}, 'a', 'default')).toBe('default');
});

it('infers the return type from the default value', () => {
const result: number | undefined = get({}, 'a', 42);
expect(result).toBe(42);
});
});
23 changes: 23 additions & 0 deletions src/utils/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function get<T>(obj: unknown, path: string | readonly string[]): T | undefined;
export function get<T>(obj: unknown, path: string | readonly string[], defaultValue: T): T;
export function get<T = unknown>(
obj: unknown,
path: string | readonly string[],
defaultValue?: T,
): T | undefined {
if (obj == null) {
return defaultValue;
}

const pathArray = typeof path === 'string' ? path.split('.') : path;
let it: any = obj;

for (const key of pathArray) {
if (it[key] === undefined) {
return defaultValue;
}
it = it[key];
}

return it;
}
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export * from './emitters';
export * as jestUtils from './jestUtils';
export { getVersion } from './getVersion';
export { aggregateLogs, logger, optimizeTracing } from './logger';
export { get } from './get';
export { set } from './set';
export { makeDeferred, Deferred } from './makeDeferred';
export { memoizeArg1 } from './memoizeArg1';
export { memoizeLast } from './memoizeLast';
Expand Down
39 changes: 39 additions & 0 deletions src/utils/isPrimitive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { isPrimitive } from './isPrimitive';

describe('isPrimitive function', () => {
it('returns true for numbers', () => {
expect(isPrimitive(42)).toBe(true);
});

it('returns true for strings', () => {
expect(isPrimitive('Hello')).toBe(true);
});

it('returns true for booleans', () => {
expect(isPrimitive(true)).toBe(true);
expect(isPrimitive(false)).toBe(true);
});

it('returns true for null', () => {
expect(isPrimitive(null)).toBe(true);
});

it('returns true for undefined', () => {
expect(isPrimitive(void 0)).toBe(true);
});

it('returns false for objects', () => {
expect(isPrimitive({})).toBe(false);
expect(isPrimitive({ key: 'value' })).toBe(false);
});

it('returns false for arrays', () => {
expect(isPrimitive([])).toBe(false);
expect(isPrimitive([1, 2, 3])).toBe(false);
});

it('returns false for functions', () => {
expect(isPrimitive(() => {})).toBe(false);
expect(isPrimitive(function () {})).toBe(false);
});
});
8 changes: 8 additions & 0 deletions src/utils/isPrimitive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function isPrimitive(value: unknown): boolean {
return (
typeof value === 'number' ||
typeof value === 'string' ||
typeof value === 'boolean' ||
value == null
);
}
40 changes: 40 additions & 0 deletions src/utils/set.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { set } from './set';

describe('set function', () => {
it('should set a value for a simple path', () => {
const obj: any = {};
set(obj, 'a', 42);
expect(obj.a).toBe(42);
});

it('should set a value for a nested path', () => {
const obj: any = {};
set(obj, 'a.b.c', 42);
expect(obj.a.b.c).toBe(42);
});

it('should set a value for a nested path with array syntax', () => {
const obj: any = {};
set(obj, ['a', 'b', 'c'], 42);
expect(obj.a.b.c).toBe(42);
});

it('should not set a value for unsafe keys', () => {
const obj: any = {};
set(obj, '__proto__.unsafe', 42);
expect(obj.unsafe).toBeUndefined();
});

it('should override existing properties', () => {
const obj = { a: 1 };
set(obj, 'a', 42);
expect(obj.a).toBe(42);
});

it.each([[null], [undefined], [42], ['string'], [true]])(
'should handle non-object targets gracefully: %j',
(obj: unknown) => {
expect(() => set(obj, 'a', 42)).not.toThrow();
},
);
});
31 changes: 31 additions & 0 deletions src/utils/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { isPrimitive } from './isPrimitive';

export function set(obj: unknown, path: string | readonly string[], value: unknown): void {
if (isPrimitive(obj)) {
return;
}

const pathArray = typeof path === 'string' ? path.split('.') : path;
let it: any = obj;

for (let i = 0; i < pathArray.length; i++) {
const key = pathArray[i];
if (isUnsafeKey(key)) {
return;
}

if (i === pathArray.length - 1) {
// If it's the last key in the path
it[key] = value;
} else {
if (it[key] === undefined) {
it[key] = {};
}
it = it[key];
}
}
}

function isUnsafeKey(key: string): boolean {
return key === '__proto__' || key === 'constructor' || key === 'prototype';
}