diff --git a/package.json b/package.json index f2c4cb2ff..7b54deb3d 100644 --- a/package.json +++ b/package.json @@ -2698,6 +2698,15 @@ "types": "./dist/objects/mutate.d.ts", "default": "./dist/objects/mutate.js" }, + "./objects/options": { + "source": "./src/objects/options.mts", + "browser": { + "types": "./dist/objects/options.d.ts", + "default": "./dist/objects/options.js" + }, + "types": "./dist/objects/options.d.ts", + "default": "./dist/objects/options.js" + }, "./objects/predicates": { "source": "./src/objects/predicates.mts", "browser": { diff --git a/src/globs/matcher.mts b/src/globs/matcher.mts index cce634288..6ef80a61e 100644 --- a/src/globs/matcher.mts +++ b/src/globs/matcher.mts @@ -8,6 +8,7 @@ import { ArrayIsArray } from '../primordials/array.mjs' import { JSONStringify } from '../primordials/json.mjs' import { ObjectKeys } from '../primordials/object.mjs' +import { fastNullObject } from '../objects/options.mjs' import { arrayToSorted } from '../polyfills/array.mjs' import { StringPrototypeStartsWith } from '../primordials/string.mjs' @@ -52,6 +53,8 @@ let matchesGlobProbed = false * const isSource = getGlobMatcher(['src/**', '!**\/*.test.ts']) * ``` */ +// `fastNullObject` sets the copied object's null prototype with a primordial. +// oxlint-disable-next-line socket/options-null-proto -- shared helper export function getGlobMatcher( glob: Pattern | Pattern[], options?: @@ -62,7 +65,7 @@ export function getGlobMatcher( } | undefined, ): (path: string) => boolean { - options = { __proto__: null, ...options } as typeof options + options = fastNullObject(options) const patterns = ArrayIsArray(glob) ? glob : [glob] // Create stable cache key by sorting patterns and option keys. // Option values that are arrays (e.g. `ignore: ['a', 'b']`) get sorted diff --git a/src/objects/options.mts b/src/objects/options.mts new file mode 100644 index 000000000..ebf3975d5 --- /dev/null +++ b/src/objects/options.mts @@ -0,0 +1,14 @@ +/** + * Shallow-copy an object, then mutate the new copy's prototype to `null`. + * + * Object spread copies only own enumerable properties. Setting the prototype + * mutates only the newly created copy, keeping it out of Object.prototype + * while retaining the fast property layout of the ordinary object literal. + * The input object is not mutated. + */ +import { ObjectSetPrototypeOf } from '../primordials/object.mjs' + +export function fastNullObject(options?: T | undefined): T { + // oxlint-disable-next-line socket/prefer-undefined-over-null -- null creates the required null prototype. + return ObjectSetPrototypeOf({ ...options }, null) as T +} diff --git a/test/unit/objects/options.test.mts b/test/unit/objects/options.test.mts new file mode 100644 index 000000000..9b94c3b43 --- /dev/null +++ b/test/unit/objects/options.test.mts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' + +import { fastNullObject } from '../../../src/objects/options.mjs' + +describe('objects/options — fastNullObject', () => { + it('copies own enumerable values onto a null-prototype object', () => { + const inherited = { inherited: 'ignored' } + const input = Object.assign(Object.create(inherited), { + dry: true, + verbose: false, + }) + + const options = fastNullObject(input) + + expect(Object.getPrototypeOf(input)).toBe(inherited) + expect(Object.getPrototypeOf(options)).toBe(null) + expect(options).toEqual({ dry: true, verbose: false }) + expect('inherited' in options).toBe(false) + }) + + it('preserves an own __proto__ key as data', () => { + const input = JSON.parse('{"__proto__":{"polluted":true}}') as Record< + string, + unknown + > + + const options = fastNullObject(input) + + expect(Object.getPrototypeOf(options)).toBe(null) + expect(Object.hasOwn(options, '__proto__')).toBe(true) + expect( + Object.getOwnPropertyDescriptor(options, '__proto__')?.value, + ).toEqual({ polluted: true }) + expect('polluted' in options).toBe(false) + }) + + it('accepts omitted options', () => { + const options = fastNullObject() + + expect(Object.getPrototypeOf(options)).toBe(null) + expect(Object.keys(options)).toEqual([]) + }) +})