diff --git a/src/hooks/rules-injector/matcher.test.ts b/src/hooks/rules-injector/matcher.test.ts index 009c19645..fce36bc4e 100644 --- a/src/hooks/rules-injector/matcher.test.ts +++ b/src/hooks/rules-injector/matcher.test.ts @@ -30,6 +30,19 @@ describe("shouldApplyRule", () => { expect(getMatcherCacheStats()).toEqual({ entries: 2 }) }) + it("#given many unique globs #when matching repeatedly #then matcher cache stays bounded", () => { + // given + const projectRoot = "/workspace/project" + + // when + for (let index = 0; index < 300; index += 1) { + shouldApplyRule({ globs: `src/file-${index}.ts` }, `${projectRoot}/src/file-${index}.ts`, projectRoot) + } + + // then + expect(getMatcherCacheStats().entries <= 256).toBe(true) + }) + it("#given matching glob #when path is under project root #then returns matching reason", () => { // given / when const result = shouldApplyRule({ globs: "src/**/*.ts" }, "/workspace/project/src/index.ts", "/workspace/project") diff --git a/src/hooks/rules-injector/matcher.ts b/src/hooks/rules-injector/matcher.ts index d68dae109..cd6995f6e 100644 --- a/src/hooks/rules-injector/matcher.ts +++ b/src/hooks/rules-injector/matcher.ts @@ -15,13 +15,24 @@ export interface MatcherCacheStats { } const PICOMATCH_OPTIONS = { dot: true, bash: true } as const +const MAX_MATCHER_CACHE_ENTRIES = 256 const matcherCache = new Map() function matcherFor(pattern: string): PathMatcher { const cached = matcherCache.get(pattern) - if (cached) return cached + if (cached) { + matcherCache.delete(pattern) + matcherCache.set(pattern, cached) + return cached + } const matcher = picomatch(pattern, PICOMATCH_OPTIONS) + if (matcherCache.size >= MAX_MATCHER_CACHE_ENTRIES) { + const oldestPattern = matcherCache.keys().next().value + if (oldestPattern !== undefined) { + matcherCache.delete(oldestPattern) + } + } matcherCache.set(pattern, matcher) return matcher }