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
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 4 additions & 1 deletion src/globs/matcher.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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?:
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/objects/options.mts
Original file line number Diff line number Diff line change
@@ -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<T extends object>(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
}
43 changes: 43 additions & 0 deletions test/unit/objects/options.test.mts
Original file line number Diff line number Diff line change
@@ -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([])
})
})
Loading