From 1eae938acb8e84c672524db18e5ab2f884c6294a Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 28 Sep 2026 13:41:22 -0400 Subject: [PATCH 1/2] feat(objects): add fast null-prototype options factory --- package.json | 9 +++++++ src/objects/options.mts | 15 +++++++++++ test/unit/objects/options.test.mts | 42 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 src/objects/options.mts create mode 100644 test/unit/objects/options.test.mts diff --git a/package.json b/package.json index 70a1ef135..763edb797 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/objects/options.mts b/src/objects/options.mts new file mode 100644 index 000000000..98fd592bd --- /dev/null +++ b/src/objects/options.mts @@ -0,0 +1,15 @@ +/** + * Copy an options object into a null-prototype object with fast properties. + * + * Object spread copies only own enumerable properties. Setting the prototype + * after construction keeps the result out of Object.prototype while retaining + * the fast property layout of the ordinary object literal. + */ +import { ObjectSetPrototypeOf } from '../primordials/object.mjs' + +export function createFastOptions( + 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..2f671e0b4 --- /dev/null +++ b/test/unit/objects/options.test.mts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' + +import { createFastOptions } from '../../../src/objects/options.mjs' + +describe('objects/options — createFastOptions', () => { + 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 = createFastOptions(input) + + 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 = createFastOptions(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 = createFastOptions() + + expect(Object.getPrototypeOf(options)).toBe(null) + expect(Object.keys(options)).toEqual([]) + }) +}) From e8210223471f24f7b101ef4da8bfe806d542cf12 Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 28 Sep 2026 16:07:39 -0400 Subject: [PATCH 2/2] perf(objects): adopt fastNullObject helper in glob matcher --- src/globs/matcher.mts | 5 ++++- src/objects/options.mts | 11 +++++------ test/unit/objects/options.test.mts | 11 ++++++----- 3 files changed, 15 insertions(+), 12 deletions(-) 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 index 98fd592bd..ebf3975d5 100644 --- a/src/objects/options.mts +++ b/src/objects/options.mts @@ -1,15 +1,14 @@ /** - * Copy an options object into a null-prototype object with fast properties. + * Shallow-copy an object, then mutate the new copy's prototype to `null`. * * Object spread copies only own enumerable properties. Setting the prototype - * after construction keeps the result out of Object.prototype while retaining - * the fast property layout of the ordinary object literal. + * 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 createFastOptions( - options?: T | undefined, -): T { +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 index 2f671e0b4..9b94c3b43 100644 --- a/test/unit/objects/options.test.mts +++ b/test/unit/objects/options.test.mts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest' -import { createFastOptions } from '../../../src/objects/options.mjs' +import { fastNullObject } from '../../../src/objects/options.mjs' -describe('objects/options — createFastOptions', () => { +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), { @@ -10,8 +10,9 @@ describe('objects/options — createFastOptions', () => { verbose: false, }) - const options = createFastOptions(input) + 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) @@ -23,7 +24,7 @@ describe('objects/options — createFastOptions', () => { unknown > - const options = createFastOptions(input) + const options = fastNullObject(input) expect(Object.getPrototypeOf(options)).toBe(null) expect(Object.hasOwn(options, '__proto__')).toBe(true) @@ -34,7 +35,7 @@ describe('objects/options — createFastOptions', () => { }) it('accepts omitted options', () => { - const options = createFastOptions() + const options = fastNullObject() expect(Object.getPrototypeOf(options)).toBe(null) expect(Object.keys(options)).toEqual([])