Skip to content

Commit 555b6f4

Browse files
committed
fix(globs): order-insensitive cache key for array-valued options
`getGlobMatcher` built its cache key with `JSON.stringify(value)` per top-level option key. For scalar values that's fine, but `ignore` is an array, and `JSON.stringify(['a', 'b'])` !== `JSON.stringify(['b', 'a'])`. Equivalent matchers that differed only in ignore-list order hit different keys, re-compiled picomatch, and evicted each other under the 100-entry LRU cap — especially painful for callers assembling ignore lists from Set iteration order or config-merge output. Sort array option values element-wise before stringifying.
1 parent 0432022 commit 555b6f4

2 files changed

Lines changed: 23 additions & 2 deletions

File tree

src/globs.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,20 @@ export function getGlobMatcher(
145145
options?: { dot?: boolean; nocase?: boolean; ignore?: string[] },
146146
): (path: string) => boolean {
147147
const patterns = Array.isArray(glob) ? glob : [glob]
148-
// Create stable cache key by sorting patterns and option keys
148+
// Create stable cache key by sorting patterns and option keys.
149+
// Option values that are arrays (e.g. `ignore: ['a', 'b']`) get sorted
150+
// element-wise so `['a', 'b']` and `['b', 'a']` hit the same entry —
151+
// otherwise equivalent matchers re-compile and evict each other under
152+
// the 100-entry cap.
149153
const sortedPatterns = [...patterns].sort()
150154
const sortedOptions = options
151155
? Object.keys(options)
152156
.sort()
153-
.map(k => `${k}:${JSON.stringify(options[k as keyof typeof options])}`)
157+
.map(k => {
158+
const value = options[k as keyof typeof options]
159+
const normalized = Array.isArray(value) ? [...value].sort() : value
160+
return `${k}:${JSON.stringify(normalized)}`
161+
})
154162
.join(',')
155163
: ''
156164
const key = `${sortedPatterns.join('|')}:${sortedOptions}`

test/unit/globs.test.mts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ describe('globs', () => {
107107
expect(matcher1).toBe(matcher2)
108108
})
109109

110+
it('caches array-valued options order-insensitively', () => {
111+
// Regression: previously JSON.stringify preserved array order, so
112+
// `{ ignore: ['a', 'b'] }` and `{ ignore: ['b', 'a'] }` produced
113+
// different cache keys and double-compiled equivalent matchers.
114+
const matcher1 = getGlobMatcher('*.js', {
115+
ignore: ['**/node_modules', '**/.git'],
116+
})
117+
const matcher2 = getGlobMatcher('*.js', {
118+
ignore: ['**/.git', '**/node_modules'],
119+
})
120+
expect(matcher1).toBe(matcher2)
121+
})
122+
110123
it('should create different matchers for different patterns', () => {
111124
const matcher1 = getGlobMatcher('*.js')
112125
const matcher2 = getGlobMatcher('*.ts')

0 commit comments

Comments
 (0)