Skip to content

Commit 5ff4cf3

Browse files
authored
fix: Workaround picomatch bug (#7195)
1 parent c5d5531 commit 5ff4cf3

File tree

9 files changed

+89
-24
lines changed

9 files changed

+89
-24
lines changed

packages/cspell-glob/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@
5959
},
6060
"dependencies": {
6161
"@cspell/url": "workspace:*",
62-
"micromatch": "^4.0.8"
62+
"picomatch": "^4.0.2"
6363
},
6464
"devDependencies": {
65-
"@types/micromatch": "^4.0.9"
65+
"@types/micromatch": "^4.0.9",
66+
"@types/picomatch": "^4.0.0",
67+
"micromatch": "^4.0.8"
6668
}
6769
}

packages/cspell-glob/src/GlobMatcher.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { fileURLToPath } from 'node:url';
33

44
import { FileUrlBuilder } from '@cspell/url';
55
import mm from 'micromatch';
6+
import pp from 'picomatch';
67
import { describe, expect, test } from 'vitest';
78

8-
import { fileOrGlobToGlob } from './globHelper.js';
9+
import { fileOrGlobToGlob, workaroundPicomatchBug } from './globHelper.js';
910
import type { GlobMatchOptions, MatcherMode } from './GlobMatcher.js';
1011
import { GlobMatcher } from './GlobMatcher.js';
1112
import type { GlobMatch, GlobPattern, GlobPatternNormalized, GlobPatternWithOptionalRoot, PathInterface } from './GlobMatcherTypes.js';
@@ -110,10 +111,36 @@ describe('Validate Micromatch assumptions', () => {
110111
${'src/*.(test|spec).ts'} | ${'src/test.ts'} | ${false}
111112
${filenameToGlob(__filename, 1)} | ${__filename} | ${true}
112113
${filenameToGlob(__filename, 2)} | ${__filename} | ${true}
114+
${'temp'} | ${'src/temp'} | ${false}
115+
${'temp'} | ${'temp'} | ${true}
113116
`(`Micromatch glob: '$glob', filename: '$filename' expected: $expectedToMatch`, ({ glob, filename, expectedToMatch }) => {
114117
const reg = mm.makeRe(glob);
115118
expect(reg.test(filename)).toEqual(expectedToMatch);
116119
expect(mm.isMatch(filename, glob, { windows: path.sep === '\\' })).toBe(expectedToMatch);
120+
121+
const normalizedFilename = filename.split(path.sep).join('/');
122+
const picomatch = pp(glob);
123+
const pmRegexp = pp.makeRe(glob);
124+
expect(pmRegexp.test(normalizedFilename)).toEqual(expectedToMatch);
125+
expect(picomatch(normalizedFilename)).toEqual(expectedToMatch);
126+
});
127+
128+
test.each`
129+
glob | filename | expectedToMatch
130+
${'constructor'} | ${'constructor'} | ${true}
131+
${'__proto__'} | ${'__proto__'} | ${true}
132+
${'toString'} | ${'toString'} | ${true}
133+
`(`Workaround bugs in Micromatch glob: '$glob', filename: '$filename' expected: $expectedToMatch`, ({ glob, filename, expectedToMatch }) => {
134+
glob = workaroundPicomatchBug(glob);
135+
const reg = mm.makeRe(glob);
136+
expect(reg.test(filename)).toEqual(expectedToMatch);
137+
expect(mm.isMatch(filename, glob, { windows: path.sep === '\\' })).toBe(expectedToMatch);
138+
139+
const normalizedFilename = filename.split(path.sep).join('/');
140+
const picomatch = pp(glob);
141+
const pmRegexp = pp.makeRe(glob);
142+
expect(pmRegexp.test(normalizedFilename)).toEqual(expectedToMatch);
143+
expect(picomatch(normalizedFilename)).toEqual(expectedToMatch);
117144
});
118145
});
119146

packages/cspell-glob/src/GlobMatcher.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import * as Path from 'node:path';
22

33
import { FileUrlBuilder } from '@cspell/url';
4-
import mm from 'micromatch';
5-
6-
import { GlobPatterns, isRelativeValueNested, normalizeGlobPatterns, normalizeGlobToRoot } from './globHelper.js';
4+
import pm from 'picomatch';
5+
6+
import {
7+
GlobPatterns,
8+
isRelativeValueNested,
9+
normalizeGlobPatterns,
10+
normalizeGlobToRoot,
11+
workaroundPicomatchBug,
12+
} from './globHelper.js';
713
import type {
814
GlobMatch,
915
GlobPattern,
@@ -231,7 +237,7 @@ function buildMatcherFn(
231237
const matchNeg = pattern.glob.match(/^!/);
232238
const glob = pattern.glob.replace(/^!/, '');
233239
const isNeg = (matchNeg && matchNeg[0].length & 1 && true) || false;
234-
const reg = mm.makeRe(glob, makeReOptions);
240+
const reg = pm.makeRe(workaroundPicomatchBug(glob), makeReOptions);
235241
const fn = pattern.glob.endsWith(suffixDir)
236242
? (filename: string) => {
237243
// Note: this is a hack to get around the limitations of globs.

packages/cspell-glob/src/__snapshots__/index.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ exports[`Validate index loads > API 1`] = `
1010
"isGlobPatternWithOptionalRoot": [Function],
1111
"isGlobPatternWithRoot": [Function],
1212
"normalizeGlobPatterns": [Function],
13+
"workaroundPicomatchBug": [Function],
1314
}
1415
`;

packages/cspell-glob/src/globHelper.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,14 @@ function filePathOrGlobToGlob(filePathOrGlob: string, root: URL, builder: FileUr
567567
return { root: builder.urlToFilePathOrHref(url), glob, isGlobalPattern };
568568
}
569569

570+
export function workaroundPicomatchBug(glob: string): string {
571+
const obj: Record<string, string> = {};
572+
return glob
573+
.split('/')
574+
.map((s) => (obj[s] ? `{${s},${s}}` : s))
575+
.join('/');
576+
}
577+
570578
export const __testing__ = {
571579
rebaseGlob,
572580
trimGlob,

packages/cspell-glob/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export {
55
isGlobPatternWithRoot,
66
normalizeGlobPatterns,
77
NormalizeOptions,
8+
workaroundPicomatchBug,
89
} from './globHelper.js';
910
export { GlobMatcher, GlobMatchOptions } from './GlobMatcher.js';
1011
export * from './GlobMatcherTypes.js';

packages/cspell/src/app/util/glob.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -289,18 +289,19 @@ describe('Validate internal functions', () => {
289289
);
290290

291291
test.each`
292-
glob | globRoot | root | expectedGlobs | file | expectedToMatch
293-
${'*.json'} | ${'.'} | ${'.'} | ${['*.json']} | ${'./package.json'} | ${true}
294-
${'*.json'} | ${'.'} | ${'.'} | ${['*.json']} | ${'./.git/package.json'} | ${false}
295-
${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/*.json']} | ${'./project/p1/package.json'} | ${true}
296-
${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/*.json']} | ${'./project/p1/src/package.json'} | ${false}
297-
${'*.json'} | ${'.'} | ${'./project/p2'} | ${['../../*.json']} | ${'./package.json'} | ${true}
298-
${'/**/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json']} | ${'./project/p2/package.json'} | ${true}
299-
${'**/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json']} | ${'./project/p2/package.json'} | ${true}
300-
${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${['../../src/*.json']} | ${'./src/data.json'} | ${true}
301-
${'**/src/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/src/*.json']} | ${'./project/p2/x/src/config.json'} | ${true}
302-
${'**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['**/src/*.json']} | ${'./project/p1/src/config.json'} | ${true}
303-
${'/**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/src/*.json']} | ${'./project/p1/src/config.json'} | ${true}
292+
glob | globRoot | root | expectedGlobs | file | expectedToMatch
293+
${'*.json'} | ${'.'} | ${'.'} | ${['*.json']} | ${'./package.json'} | ${true}
294+
${'*.json'} | ${'.'} | ${'.'} | ${['*.json']} | ${'./.git/package.json'} | ${false}
295+
${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/*.json']} | ${'./project/p1/package.json'} | ${true}
296+
${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/*.json']} | ${'./project/p1/src/package.json'} | ${false}
297+
${'*.json'} | ${'.'} | ${'./project/p2'} | ${['../../*.json']} | ${'./package.json'} | ${true}
298+
${'/**/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json']} | ${'./project/p2/package.json'} | ${true}
299+
${'**/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json']} | ${'./project/p2/package.json'} | ${true}
300+
${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${['../../src/*.json']} | ${'./src/data.json'} | ${true}
301+
${'**/src/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/src/*.json']} | ${'./project/p2/x/src/config.json'} | ${true}
302+
${'**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['**/src/*.json']} | ${'./project/p1/src/config.json'} | ${true}
303+
${'/**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/src/*.json']} | ${'./project/p1/src/config.json'} | ${true}
304+
${'**/constructor/**/*.json'} | ${'.'} | ${'.'} | ${['**/constructor/**/*.json']} | ${'./constructor/package.json'} | ${true}
304305
`(
305306
'mapGlobToRoot include "$glob"@"$globRoot" -> "$root" = "$expectedGlobs"',
306307
({ glob, globRoot, root, expectedGlobs, file, expectedToMatch }: TestMapGlobToRoot) => {

packages/cspell/src/app/util/glob.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { posix } from 'node:path';
44

55
import type { CSpellUserSettings, Glob } from '@cspell/cspell-types';
66
import type { GlobPatternWithRoot } from 'cspell-glob';
7-
import { fileOrGlobToGlob, GlobMatcher } from 'cspell-glob';
7+
import { fileOrGlobToGlob, GlobMatcher, workaroundPicomatchBug } from 'cspell-glob';
88
import type { GlobOptions as TinyGlobbyOptions } from 'tinyglobby';
9-
import { glob } from 'tinyglobby';
9+
import { glob as tinyGlob } from 'tinyglobby';
1010

1111
import { clean } from './util.js';
1212

@@ -187,3 +187,11 @@ export async function normalizeFileOrGlobsToRoot(globs: Glob[], root: string): P
187187
const adjustedGlobs = await Promise.all(globs.map((g) => adjustPossibleDirectory(g, root)));
188188
return normalizeGlobsToRoot(adjustedGlobs, root, false);
189189
}
190+
191+
function glob(patterns: string | string[], options: TinyGlobbyOptions): Promise<string[]> {
192+
patterns =
193+
typeof patterns === 'string'
194+
? workaroundPicomatchBug(patterns)
195+
: patterns.map((g) => workaroundPicomatchBug(g));
196+
return tinyGlob(patterns, options);
197+
}

pnpm-lock.yaml

Lines changed: 14 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)