diff --git a/packages/cspell-glob/src/GlobMatcher.test.ts b/packages/cspell-glob/src/GlobMatcher.test.ts index f64dd13ac62..7c16090a348 100644 --- a/packages/cspell-glob/src/GlobMatcher.test.ts +++ b/packages/cspell-glob/src/GlobMatcher.test.ts @@ -33,6 +33,8 @@ const pathNames = new Map([ [pathPosix, 'Posix'], ]); +const oc = expect.objectContaining; + function r(...parts: string[]) { return path.resolve(...parts); } @@ -211,7 +213,7 @@ describe('Tests .gitignore file contents', () => { ${root + 'dist/code.ts'} | ${{ matched: true, pattern: p('**/dist/**'), isNeg: false }} | ${'Ensure that `dest` .ts files are not allowed'} ${root + 'src/code.js'} | ${{ matched: true, pattern: p('**/*.js'), isNeg: false }} | ${'Ensure that no .js files are allowed'} ${root + 'dist/settings.js'} | ${{ matched: false, pattern: p('!**/settings.js'), isNeg: true }} | ${'Ensure that settings.js is kept'} - `('match && matchEx "$comment" File: "$filename" $expected', ({ filename, expected }: TestCase) => { + `('match && matchEx $comment File: $filename $expected', ({ filename, expected }: TestCase) => { expected = typeof expected === 'boolean' ? { matched: expected } : expected; expect(matcher.match(filename)).toBe(expected.matched); expect(matcher.matchEx(filename)).toEqual(expect.objectContaining(expected)); @@ -262,7 +264,7 @@ describe('Tests .gitignore like file contents', () => { ${root + 'dist/code.ts'} | ${{ matched: true, pattern: p('**/dist/**'), isNeg: false }} | ${'Ensure that `dest` .ts files are not allowed'} ${root + 'src/code.js'} | ${{ matched: true, pattern: p('**/*.js'), isNeg: false }} | ${'Ensure that no .js files are allowed'} ${root + 'dist/settings.js'} | ${{ matched: false, pattern: p('!**/settings.js'), isNeg: true }} | ${'Ensure that settings.js is kept'} - `('match && matchEx "$comment" File: "$filename" $expected', ({ filename, expected }: TestCase) => { + `('match && matchEx $comment File: $filename $expected', ({ filename, expected }: TestCase) => { expected = typeof expected === 'boolean' ? { matched: expected } : expected; expect(matcher.match(filename)).toBe(expected.matched); expect(matcher.matchEx(filename)).toEqual(expect.objectContaining(expected)); @@ -501,19 +503,19 @@ describe('normalizing globs', () => { test.each` glob | globRoot | root | expectedGlobs | file | expectedToMatch - ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${[]} | ${''} | ${false} + ${'src/*.json'} | ${'.'} | ${'./p2'} | ${['../src/*.json', '../src/*.json/**']} | ${''} | ${false} ${'**'} | ${'.'} | ${'.'} | ${['**']} | ${'./package.json'} | ${true} ${'*.json'} | ${'.'} | ${'.'} | ${['**/*.json', '**/*.json/**']} | ${'./package.json'} | ${true} ${'*.json'} | ${'.'} | ${'.'} | ${['**/*.json', '**/*.json/**']} | ${'./.git/package.json'} | ${true} ${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/*.json', 'project/p1/**/*.json/**']} | ${'./project/p1/package.json'} | ${true} ${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/*.json', 'project/p1/**/*.json/**']} | ${'./project/p1/src/package.json'} | ${true} ${'*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json', '**/*.json/**']} | ${'./project/p2/package.json'} | ${true} - ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${[]} | ${''} | ${false} + ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${['../../src/*.json', '../../src/*.json/**']} | ${''} | ${false} ${'**/src/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/src/*.json', '**/src/*.json/**']} | ${'./project/p2/x/src/config.json'} | ${true} ${'**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['**/src/*.json', '**/src/*.json/**']} | ${'./project/p1/src/config.json'} | ${true} ${'/**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/src/*.json', 'project/p1/**/src/*.json/**']} | ${'./project/p1/src/config.json'} | ${true} ${'/docs/types/cspell-types'} | ${gitRoot} | ${gitRoot} | ${['docs/types/cspell-types', 'docs/types/cspell-types/**']} | ${r(gitRoot, './docs/types/cspell-types/assets/main.js')} | ${true} - `('mapGlobToRoot exclude "$glob"@"$globRoot" -> "$root" = "$expectedGlobs"', ({ glob, globRoot, root, expectedGlobs, file, expectedToMatch }: TestMapGlobToRoot) => { + `('mapGlobToRoot exclude $glob@$globRoot -> $root = $expectedGlobs', ({ glob, globRoot, root, expectedGlobs, file, expectedToMatch }: TestMapGlobToRoot) => { globRoot = path.resolve(globRoot); root = path.resolve(root); file = path.resolve(file); @@ -534,14 +536,14 @@ describe('normalizing globs', () => { ${'*.json'} | ${'.'} | ${'.'} | ${['*.json']} | ${'./.git/package.json'} | ${false} ${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/*.json']} | ${'./project/p1/package.json'} | ${true} ${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/*.json']} | ${'./project/p1/src/package.json'} | ${false} - ${'*.json'} | ${'.'} | ${'./project/p2'} | ${[]} | ${'./project/p2/package.json'} | ${false} + ${'*.json'} | ${'.'} | ${'./project/p2'} | ${['../../*.json']} | ${'./project/p2/package.json'} | ${false} ${'/**/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json']} | ${'./project/p2/package.json'} | ${true} ${'**/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json']} | ${'./project/p2/package.json'} | ${true} - ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${[]} | ${''} | ${false} + ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${['../../src/*.json']} | ${''} | ${false} ${'**/src/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/src/*.json']} | ${'./project/p2/x/src/config.json'} | ${true} ${'**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['**/src/*.json']} | ${'./project/p1/src/config.json'} | ${true} ${'/**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/src/*.json']} | ${'./project/p1/src/config.json'} | ${true} - `('mapGlobToRoot include "$glob"@"$globRoot" -> "$root" = "$expectedGlobs"', ({ glob, globRoot, root, expectedGlobs, file, expectedToMatch }: TestMapGlobToRoot) => { + `('mapGlobToRoot include $glob@$globRoot -> $root = $expectedGlobs', ({ glob, globRoot, root, expectedGlobs, file, expectedToMatch }: TestMapGlobToRoot) => { globRoot = path.resolve(globRoot); root = path.resolve(root); file = path.resolve(file); @@ -557,6 +559,36 @@ describe('normalizing globs', () => { }); }); +describe('Build GlobMatcher', () => { + test.each` + glob | root | isExclude | expected + ${'*.json'} | ${'.'} | ${false} | ${['*.json']} + ${'src/**/*.ts'} | ${'.'} | ${false} | ${['src/**/*.ts']} + ${'../src/**/*.ts'} | ${'.'} | ${false} | ${['../src/**/*.ts']} + ${'./src/**/*.ts'} | ${'.'} | ${false} | ${['./src/**/*.ts']} + ${'./src/../test/**/*.ts'} | ${'.'} | ${false} | ${['./src/../test/**/*.ts']} + ${'*.json'} | ${'.'} | ${true} | ${['**/*.json', '**/*.json/**']} + `('patternsNormalizedToRoot $glob, $root, $isExclude', ({ glob, root, isExclude, expected }) => { + root = path.normalize(path.resolve(root || '') + '/'); + const gm = new GlobMatcher([glob], { root, mode: isExclude ? 'exclude' : 'include' }); + expect(gm.patternsNormalizedToRoot).toEqual((expected as string[]).map((glob) => oc({ glob, root }))); + }); + + test.each` + glob | root | isExclude | expected + ${'*.json'} | ${'.'} | ${false} | ${['*.json']} + ${'src/**/*.ts'} | ${'.'} | ${false} | ${['src/**/*.ts']} + ${'../src/**/*.ts'} | ${'.'} | ${false} | ${['../src/**/*.ts']} + ${'./src/**/*.ts'} | ${'.'} | ${false} | ${['./src/**/*.ts']} + ${'./src/../test/**/*.ts'} | ${'.'} | ${false} | ${['./src/../test/**/*.ts']} + ${'*.json'} | ${'.'} | ${true} | ${['**/*.json', '**/*.json/**']} + `('patterns $glob, $root, $isExclude', ({ glob, root, isExclude, expected }) => { + root = path.normalize(path.resolve(root || '') + '/'); + const gm = new GlobMatcher([glob], { root, mode: isExclude ? 'exclude' : 'include' }); + expect(gm.patterns).toEqual((expected as string[]).map((glob) => oc({ glob, root }))); + }); +}); + type TestCase = [patterns: GlobPattern[] | GlobPattern, root: string | undefined, filename: string, expected: boolean, description: string]; function tests(): TestCase[] { diff --git a/packages/cspell-glob/src/globHelper.test.ts b/packages/cspell-glob/src/globHelper.test.ts index 20902a4d9d6..d279c186236 100644 --- a/packages/cspell-glob/src/globHelper.test.ts +++ b/packages/cspell-glob/src/globHelper.test.ts @@ -71,7 +71,7 @@ describe('Validate fileOrGlobToGlob', () => { ${'e:\\user\\projects\\spell\\package.json'} | ${'.'} | ${win32} | ${g('/package.json', pw('./spell/'))} | ${'Directory matching root.'} ${'/user/tester/projects/**/*.json'} | ${'.'} | ${posix} | ${g('**/*.json', pp('/user/tester/projects/'))} | ${'A glob like path not matching the root.'} ${'C:\\user\\tester\\projects\\**\\*.json'} | ${'.'} | ${win32} | ${g('**/*.json', pw('C:/user/tester/projects/'))} | ${'A glob like path not matching the root.'} - `('fileOrGlobToGlob file: "$file" root: "$root" $comment', ({ file, root, path, expected }) => { + `('fileOrGlobToGlob file: $file root: $root $comment', ({ file, root, path, expected }) => { root = p(root, path); const r = fileOrGlobToGlob(file, root, path); expect(r).toEqual(expected); @@ -81,7 +81,7 @@ describe('Validate fileOrGlobToGlob', () => { file | root | path | expected | comment ${'*.json'} | ${uph('.', pathPosix)} | ${pathPosix} | ${g('*.json', p('./', pathPosix))} | ${'posix'} ${'*.json'} | ${uph('.', pathWin32)} | ${pathWin32} | ${g('*.json', p('./', pathWin32))} | ${'win32'} - `('fileOrGlobToGlob file: "$file" root: "$root" $comment', ({ file, root, path, expected }) => { + `('fileOrGlobToGlob file: $file root: $root $comment', ({ file, root, path, expected }) => { root = p(root, path); const r = fileOrGlobToGlob(file, root, path || Path); expect(r).toEqual(expected); @@ -100,7 +100,7 @@ describe('Validate fileOrGlobToGlob', () => { ${{ glob: '/**/*.json', root: pp('./data') }} | ${'.'} | ${posix} | ${g('/**/*.json', pp('./data/'))} | ${'posix'} ${{ glob: '*.json', root: '${cwd}' }} | ${'.'} | ${posix} | ${g('*.json', '${cwd}')} | ${'posix'} ${{ glob: '${cwd}/*.json', root: pp('./data') }} | ${'.'} | ${posix} | ${g('/*.json', '${cwd}')} | ${'posix'} - `('fileOrGlobToGlob glob: "$glob" root: "$root" $comment', ({ glob, root, path, expected }) => { + `('fileOrGlobToGlob glob: $glob root: $root $comment', ({ glob, root, path, expected }) => { root = p(root, path); const r = fileOrGlobToGlob(glob, root, path); expect(r).toEqual(expected); @@ -193,24 +193,29 @@ describe('Validate Glob Normalization to root', () => { } test.each` - glob | base | file | expected - ${'**/*.json'} | ${''} | ${'cfg.json'} | ${'**/*.json'} - ${'**/*.json'} | ${''} | ${'project/cfg.json'} | ${'**/*.json'} - ${'*.json'} | ${''} | ${'cfg.json'} | ${'*.json'} - ${'*.json'} | ${''} | ${'!cfg.js'} | ${'*.json'} - ${'package.json'} | ${'/'} | ${'package.json'} | ${'package.json'} - ${'*/package.json'} | ${'/'} | ${'config/package.json'} | ${'*/package.json'} - ${'**/*.json'} | ${'project'} | ${'cfg.json'} | ${'**/*.json'} - ${'*.json'} | ${'project'} | ${'cfg.json'} | ${undefined} - ${'project/*.json'} | ${'project'} | ${'cfg.json'} | ${'*.json'} - ${'*/*.json'} | ${'project'} | ${'cfg.json'} | ${'*.json'} - ${'*/src/**/*.js'} | ${'project'} | ${'src/cfg.js'} | ${'src/**/*.js'} - ${'**/project/src/**/*.js'} | ${'project'} | ${'!src/cfg.js'} | ${'**/project/src/**/*.js'} - ${'**/{node_modules,node_modules/**}'} | ${''} | ${'x/node_modules/cs/pkg.json'} | ${'**/{node_modules,node_modules/**}'} - `('rebaseGlob "$glob" to "$base" with file "$file"', ({ glob, base, file, expected }) => { - const r = rebaseGlob(glob, base); + glob | globRoot | root | file | expected + ${'**/*.json'} | ${''} | ${''} | ${'cfg.json'} | ${'**/*.json'} + ${'**/*.json'} | ${''} | ${''} | ${'project/cfg.json'} | ${'**/*.json'} + ${'*.json'} | ${''} | ${''} | ${'cfg.json'} | ${'*.json'} + ${'*.json'} | ${''} | ${''} | ${'!cfg.js'} | ${'*.json'} + ${'package.json'} | ${'.'} | ${'.'} | ${'package.json'} | ${'package.json'} + ${'*/package.json'} | ${'.'} | ${'.'} | ${'config/package.json'} | ${'*/package.json'} + ${'**/*.json'} | ${'.'} | ${'project'} | ${'cfg.json'} | ${'**/*.json'} + ${'*.json'} | ${'.'} | ${'project'} | ${'cfg.json'} | ${'../*.json'} + ${'project/*.json'} | ${'.'} | ${'project'} | ${'cfg.json'} | ${'*.json'} + ${'*/*.json'} | ${'.'} | ${'project'} | ${'cfg.json'} | ${'*.json'} + ${'*/src/**/*.js'} | ${'.'} | ${'project'} | ${'src/cfg.js'} | ${'src/**/*.js'} + ${'**/project/src/**/*.js'} | ${'project'} | ${'.'} | ${'!src/cfg.js'} | ${'project/**/project/src/**/*.js'} + ${'**/project/src/**/*.js'} | ${''} | ${'project'} | ${'!src/cfg.js'} | ${'**/project/src/**/*.js'} + ${'**/{node_modules,node_modules/**}'} | ${''} | ${''} | ${'x/node_modules/cs/pkg.json'} | ${'**/{node_modules,node_modules/**}'} + `('rebaseGlob $glob to $globRoot, $root with file $file', ({ glob, globRoot, root, file, expected }) => { + root = posix.resolve(root); + globRoot = posix.resolve(globRoot); + const relRootToGlob = posix.relative(root, globRoot); + const relGlobToRoot = posix.relative(globRoot, root); + const r = rebaseGlob(glob, relRootToGlob, relGlobToRoot); expect(r).toEqual(expected); - if (r) { + if (!r.startsWith('../')) { const shouldMatch = !file.startsWith('!'); const filename = file.replace(/!$/, ''); @@ -218,6 +223,34 @@ describe('Validate Glob Normalization to root', () => { } }); + test.each` + glob | globRoot | root | expected + ${'**/*.json'} | ${'.'} | ${'.'} | ${'**/*.json'} + ${'*.json'} | ${'.'} | ${'.'} | ${'*.json'} + ${'package.json'} | ${'.'} | ${'proj'} | ${'../package.json'} + ${'package.json'} | ${'proj'} | ${'.'} | ${'proj/package.json'} + ${'*/package.json'} | ${'.'} | ${'proj'} | ${'package.json'} + ${'**/*.json'} | ${'.'} | ${'proj'} | ${'**/*.json'} + ${'**/*.json'} | ${'proj'} | ${'.'} | ${'proj/**/*.json'} + ${'*.json'} | ${'project'} | ${'project'} | ${'*.json'} + ${'*.json'} | ${'../project'} | ${'../test'} | ${'../project/*.json'} + ${'/*.json'} | ${'../project'} | ${'../test'} | ${'../project/*.json'} + ${'**/*.json'} | ${'project'} | ${'test'} | ${'../project/**/*.json'} + ${'project/*.json'} | ${'.'} | ${'project/'} | ${'*.json'} + ${'*/*.json'} | ${'.'} | ${'project/'} | ${'*.json'} + ${'*/src/**/*.js'} | ${'.'} | ${'project/'} | ${'src/**/*.js'} + ${'src/**/*.js'} | ${'project'} | ${'test'} | ${'../project/src/**/*.js'} + ${'**/project/src/**/*.js'} | ${'.'} | ${'project'} | ${'**/project/src/**/*.js'} + ${'**/{node_modules,node_modules/**}'} | ${''} | ${''} | ${'**/{node_modules,node_modules/**}'} + `('rebaseGlob $glob to $globRoot -> $root', ({ glob, globRoot, root, expected }) => { + root = posix.resolve(root); + globRoot = posix.resolve(globRoot); + const relRootToGlob = posix.relative(root, globRoot); + const relGlobToRoot = posix.relative(globRoot, root); + const r = rebaseGlob(glob, relRootToGlob, relGlobToRoot); + expect(r).toEqual(expected); + }); + test.each` glob | expected ${''} | ${''} @@ -229,7 +262,7 @@ describe('Validate Glob Normalization to root', () => { ${' \\ space before and after\\ # comment '} | ${'\\ space before and after\\ '} ${'\\'} | ${'\\'} ${'\\a'} | ${'\\a'} - `('trimGlob "$glob"', ({ glob, expected }) => { + `('trimGlob $glob', ({ glob, expected }) => { expect(trimGlob(glob)).toBe(expected); }); @@ -248,8 +281,8 @@ describe('Validate Glob Normalization to root', () => { ${gp('*.json')} | ${'cspell-glob/cfg.json'} | ${'..'} | ${eg({ glob: 'cspell-glob/*.json', root: '../' }, Path)} | ${'root above'} ${gp('*.json', '.', pathPosix)} | ${'cspell/cfg.json'} | ${'..'} | ${eg({ glob: 'cspell/*.json', root: '../' }, pathPosix)} | ${'root above'} ${gp('*.json', '.', pathWin32)} | ${'cspell/cfg.json'} | ${'..'} | ${eg({ glob: 'cspell/*.json', root: '../' }, pathWin32)} | ${'root above'} - ${gp('*.json')} | ${'cfg.json'} | ${'deeper'} | ${eg({ glob: '*.json', root: './' }, Path)} | ${'root below, cannot change'} - ${gp('*.json', '.', pathPosix)} | ${'cfg.json'} | ${'deeper'} | ${eg({ glob: '*.json', root: './' }, pathPosix)} | ${'root below, cannot change'} + ${gp('*.json')} | ${'cfg.json'} | ${'deeper'} | ${eg({ glob: '../*.json', root: 'deeper/' }, Path)} | ${'root below, cannot change'} + ${gp('*.json', '.', pathPosix)} | ${'cfg.json'} | ${'deeper'} | ${eg({ glob: '../*.json', root: 'deeper/' }, pathPosix)} | ${'root below, cannot change'} ${gp('**/*.json', '.', pathWin32)} | ${'cfg.json'} | ${'deeper'} | ${eg({ glob: '**/*.json', root: 'deeper/', ...globalGlob }, pathWin32)} | ${'root below, globstar'} ${gp('deeper/*.json')} | ${'cfg.json'} | ${'deeper'} | ${eg({ glob: '*.json', root: 'deeper/' }, Path)} | ${'root below, matching'} ${gp('deeper/*.json', '.', pathPosix)} | ${'cfg.json'} | ${'deeper'} | ${eg({ glob: '*.json', root: 'deeper/' }, pathPosix)} | ${'root below, matching'} @@ -276,7 +309,7 @@ describe('Validate Glob Normalization to root', () => { file = file.replace(/^!/, ''); file = builder.relative(rootURL, builder.toFileURL(file, rootURL)); - expect(mm.isMatch(file, result.glob)).toBe(shouldMatch); + !result.glob.startsWith('../') && expect(mm.isMatch(file, result.glob)).toBe(shouldMatch); }, ); @@ -326,7 +359,7 @@ describe('Validate Glob Normalization to root', () => { ${mg('/node_modules/')} | ${'project'} | ${e(mGlob(gg('node_modules/**/*'), { rawGlob: '/node_modules/' }))} | ${'/node_modules/'} ${mg({ glob: '/node_modules/' })} | ${'project'} | ${e(mGlob(gg('node_modules/**/*'), { rawGlob: '/node_modules/' }))} | ${'/node_modules/'} ${mg('i18/en_US')} | ${'project'} | ${e(mGlob(gg('i18/en_US', 'i18/en_US/**'), { rawGlob: 'i18/en_US' }))} | ${'i18/en_US'} - `('tests normalization nested "$comment" root: "$root"', ({ globs, root, expectedGlobs }: TestCase) => { + `('tests normalization nested $comment root: $root', ({ globs, root, expectedGlobs }: TestCase) => { root = Path.resolve(root); const r = normalizeGlobPatterns(globs, { root, nested: true, nodePath: Path }); expect(r).toEqual(expectedGlobs); @@ -350,7 +383,7 @@ describe('Validate Glob Normalization to root', () => { ${j(mg('*.json', '../tests/a'))} | ${'project'} | ${e(mGlob('*.json', { rawGlob: '*.json', root: '../tests/a/' }))} | ${''} ${j(mg('*/*.json', 'project/a'))} | ${'project'} | ${e(gg('a/*/*.json', 'a/*/*.json/**'))} | ${'nested a/*/*.json'} ${j(mg('*/*.json', '.'))} | ${'project'} | ${e(gg('*.json', '*.json/**'))} | ${'nested */*.json'} - `('tests normalization to root nested "$comment" root: "$root"', ({ globs, root, expectedGlobs }: TestCase) => { + `('tests normalization to root nested $comment root: $root', ({ globs, root, expectedGlobs }: TestCase) => { root = Path.resolve(root); const r = normalizeGlobPatterns(globs, { root, nested: true, nodePath: Path }).map((p) => normalizeGlobToRoot(p, root, Path), @@ -364,7 +397,7 @@ describe('Validate Glob Normalization to root', () => { ${mg('**')} | ${'.'} | ${e(mGlob(gg('**'), globalGlob))} | ${'**'} ${j(mg('*.json', 'project/a'), mg('*.ts', '.'))} | ${'.'} | ${e(mGlob(gg('project/a/*.json'), { rawGlob: '*.json' }), gg('*.ts'))} | ${'Sub dir glob.'} ${j(mg('*.json', '../tests/a'), mg('*.ts', '.'))} | ${'.'} | ${e(mGlob(gg('*.json'), { root: '../tests/a/' }), gg('*.ts'))} | ${'Glob not in root is not changed.'} - ${mg('*.json')} | ${'project'} | ${e(mGlob(gg('*.json'), { root: './' }))} | ${'Root deeper than glob'} + ${mg('*.json')} | ${'project'} | ${e(mGlob(gg('../*.json'), { root: './project/' }))} | ${'Root deeper than glob'} ${j(mg('*.json', 'project/a'))} | ${'project'} | ${e(mGlob(gg('a/*.json'), { rawGlob: '*.json' }))} | ${'Root in the middle.'} ${j(mg('/node_modules', 'project/a'))} | ${'project'} | ${e(mGlob(gg('a/node_modules'), { rawGlob: '/node_modules' }))} | ${'Root in the middle. /node_modules'} ${j(mg('*.json', '../tests/a'))} | ${'project'} | ${e({ glob: '*.json', root: '../tests/a/' })} | ${'Glob not in root is not changed.'} @@ -376,7 +409,7 @@ describe('Validate Glob Normalization to root', () => { ${j(mg('project/*/*.json', '.'))} | ${'project/sub'} | ${e({ glob: '*.json', root: 'project/sub/' })} | ${'nested project/*/*.json'} ${j(mg('node_modules', '.'))} | ${'.'} | ${e({ glob: 'node_modules', root: './' })} | ${'node_modules'} ${j(mg('node_modules/', '.'))} | ${'.'} | ${e({ glob: 'node_modules/**/*', root: './' })} | ${'node_modules/'} - `('tests normalization to root not nested "$comment" root: "$root"', ({ globs, root, expectedGlobs }: TestCase) => { + `('tests normalization to root not nested $comment root: $root', ({ globs, root, expectedGlobs }: TestCase) => { root = Path.resolve(root); const r = normalizeGlobPatterns(globs, { root, nested: false, nodePath: Path }).map((p) => { return normalizeGlobToRoot(p, root, Path); @@ -415,7 +448,7 @@ describe('Validate Glob Normalization to root', () => { ${{ glob: '*.ts', root: '${cwd}/a' }} | ${nOpts({ cwd: 'myCwd' })} | ${[gN('*.ts', nOpts().nodePath.resolve('myCwd/a'), '*.ts', '${cwd}/a')]} ${{ glob: '${cwd}/*.ts', root: 'a' }} | ${nOpts({ cwd: 'myCwd' })} | ${[gN('*.ts', nOpts().nodePath.resolve('myCwd'), '${cwd}/*.ts', 'a')]} ${{ glob: 'a/*.ts', root: '${cwd}/myRoot' }} | ${nOpts({ root: 'otherRoot' })} | ${[gN('a/*.ts', nOpts().nodePath.resolve('myRoot'), 'a/*.ts', '${cwd}/myRoot')]} - `('normalizeGlobPattern glob: "$glob", options: $options', ({ glob, options, expected }) => { + `('normalizeGlobPattern glob: $glob, options: $options', ({ glob, options, expected }) => { expect(normalizeGlobPattern(glob, options)).toEqual(expected); }); }); @@ -463,7 +496,7 @@ describe('Validate minimatch assumptions', () => { ${'{*.js,!index.js}'} | ${'index.js'} | ${{}} | ${true} | ${'nested negative does not work as expected'} ${'{!!index.js,*.ts}'} | ${'index.js'} | ${{}} | ${false} | ${'nested negative does not work as expected'} `( - 'assume glob "$pattern" matches "$file" is $expected - $comment', + 'assume glob $pattern matches $file is $expected - $comment', ({ pattern, file, options, expected }: TestCase) => { const r = mm.isMatch(file, pattern, options); expect(r).toBe(expected); diff --git a/packages/cspell-glob/src/globHelper.ts b/packages/cspell-glob/src/globHelper.ts index 2efaebd0c06..f44afa0840f 100644 --- a/packages/cspell-glob/src/globHelper.ts +++ b/packages/cspell-glob/src/globHelper.ts @@ -327,35 +327,55 @@ export function normalizeGlobToRoot( // The root is under the glob root // The more difficult case, the glob is higher than the root // A best effort is made, but does not do advanced matching. - const relGlob = (relFromGlobToRoot + '/').replaceAll('//', '/'); - const rebasedGlob = rebaseGlob(g, relGlob); + const rebasedGlob = rebaseGlob(g, nRel(relFromRootToGlob), nRel(relFromGlobToRoot)); return rebasedGlob ? { ...glob, glob: prefix + rebasedGlob, root } : glob; } +function nRel(rel: string): string { + return rel.endsWith('/') ? rel : rel + '/'; +} + export function isRelativeValueNested(rel: string): boolean { return !rel || !(rel === '..' || rel.startsWith('../') || rel.startsWith('/')); } /** - * Rebase a glob string to a new prefix + * Rebase a glob string to a new root. * @param glob - glob string - * @param rebaseTo - glob prefix + * @param fromRootToGlob - relative path from root to globRoot + * @param fromGlobToRoot - relative path from globRoot to root */ -export function rebaseGlob(glob: string, rebaseTo: string): string | undefined { - if (!rebaseTo || rebaseTo === '/') return glob; - if (glob.startsWith('**')) return glob; - rebaseTo = rebaseTo.endsWith('/') ? rebaseTo : rebaseTo + '/'; +export function rebaseGlob(glob: string, fromRootToGlob: string, fromGlobToRoot: string): string { + if (!fromGlobToRoot || fromGlobToRoot === '/') return glob; + if (fromRootToGlob.startsWith('../') && !fromGlobToRoot.startsWith('../') && glob.startsWith('**')) return glob; + fromRootToGlob = nRel(fromRootToGlob); + fromGlobToRoot = nRel(fromGlobToRoot); + + const relToParts = fromRootToGlob.split('/'); + const relFromParts = fromGlobToRoot.split('/'); + + // console.warn('rebaseGlob 1: %o', { glob, fromRootToGlob, fromGlobToRoot, relToParts, relFromParts }); + + if (glob.startsWith(fromGlobToRoot) && fromRootToGlob === '../'.repeat(relToParts.length - 1)) { + return glob.slice(fromGlobToRoot.length); + } + + const lastRelIdx = relToParts.findIndex((s) => s !== '..'); + const lastRel = lastRelIdx < 0 ? relToParts.length : lastRelIdx; + const globParts = [...relToParts.slice(lastRel).filter((a) => a), ...glob.split('/')]; + relToParts.length = lastRel; + + // console.warn('rebaseGlob 2: %o', { glob, fromRootToGlob, fromGlobToRoot, globParts, relToParts, relFromParts }); - if (glob.startsWith(rebaseTo)) { - return glob.slice(rebaseTo.length); + if (fromRootToGlob.startsWith('../') && relFromParts.length !== relToParts.length + 1) { + return fromRootToGlob + (glob.startsWith('/') ? glob.slice(1) : glob); } - const relParts = rebaseTo.split('/'); - const globParts = glob.split('/'); + // console.warn('rebaseGlob 3: %o', { glob, fromRootToGlob, fromGlobToRoot, globParts, relToParts, relFromParts }); - for (let i = 0; i < relParts.length && i < globParts.length; ++i) { - const relSeg = relParts[i]; + for (let i = 0; i < relFromParts.length && i < globParts.length; ++i) { + const relSeg = relFromParts[i]; const globSeg = globParts[i]; // the empty segment due to the end relGlob / allows for us to test against an empty segment. if (!relSeg || globSeg === '**') { @@ -365,7 +385,7 @@ export function rebaseGlob(glob: string, rebaseTo: string): string | undefined { break; } } - return undefined; + return fromRootToGlob + (glob.startsWith('/') ? glob.slice(1) : glob); } /** @@ -541,9 +561,8 @@ function fixPatternRelativeToRoot(glob: GlobPatternWithRoot, root: URL, builder: function filePathOrGlobToGlob(filePathOrGlob: string, root: URL, builder: FileUrlBuilder): GlobPatternWithRoot { const isGlobalPattern = isGlobalGlob(filePathOrGlob); - const { path, glob } = builder.isAbsolute(filePathOrGlob) - ? splitGlob(filePathOrGlob) - : splitGlobRel(filePathOrGlob); + const isAbsolute = builder.isAbsolute(filePathOrGlob); + const { path, glob } = isAbsolute ? splitGlob(filePathOrGlob) : splitGlobRel(filePathOrGlob); const url = builder.toFileDirURL(path || './', root); return { root: builder.urlToFilePathOrHref(url), glob, isGlobalPattern }; } diff --git a/packages/cspell/src/app/util/glob.test.ts b/packages/cspell/src/app/util/glob.test.ts index 69666848fdf..51fa5019d48 100644 --- a/packages/cspell/src/app/util/glob.test.ts +++ b/packages/cspell/src/app/util/glob.test.ts @@ -191,6 +191,38 @@ describe('Validate internal functions', () => { expect(r).toEqual({ globs: ['*/test files/', 'node_modules', '**/*.dat'], source: 'arguments' }); }); + test.each` + glob | root | exclude | expectedGlobs + ${'src/*.json'} | ${'./project/p2'} | ${true} | ${['src/*.json', 'src/*.json/**']} + ${'**'} | ${'.'} | ${true} | ${['**']} + ${'*.json'} | ${'.'} | ${true} | ${['**/*.json', '**/*.json/**']} + ${'*.json'} | ${'./project/p2'} | ${true} | ${['**/*.json', '**/*.json/**']} + ${'src/*.json'} | ${'./project/p2'} | ${true} | ${['src/*.json', 'src/*.json/**']} + ${'**/src/*.json'} | ${'./project/p2'} | ${true} | ${['**/src/*.json', '**/src/*.json/**']} + ${'**/src/*.json'} | ${'.'} | ${true} | ${['**/src/*.json', '**/src/*.json/**']} + ${'/**/src/*.json'} | ${'.'} | ${true} | ${['**/src/*.json', '**/src/*.json/**']} + ${'src/*.json'} | ${'./project/p2'} | ${false} | ${['src/*.json']} + ${'**'} | ${'.'} | ${false} | ${['**']} + ${'*.json'} | ${'.'} | ${false} | ${['*.json']} + ${'*.json'} | ${'./project/p2'} | ${false} | ${['*.json']} + ${'src/*.json'} | ${'./project/p2'} | ${false} | ${['src/*.json']} + ${'**/src/*.json'} | ${'./project/p2'} | ${false} | ${['**/src/*.json']} + ${'**/src/*.json'} | ${'.'} | ${false} | ${['**/src/*.json']} + ${'/**/src/*.json'} | ${'.'} | ${false} | ${['**/src/*.json']} + ${'!../src/*.ts'} | ${'.'} | ${false} | ${['!../src/*.ts']} + ${'../src/*.ts'} | ${'.'} | ${false} | ${['../src/*.ts']} + ${'../src/../test/**/*.ts'} | ${'.'} | ${false} | ${['../test/**/*.ts']} + ${'../src/../../test/**/*.ts'} | ${'.'} | ${false} | ${['../../test/**/*.ts']} + ${path.resolve('src/*.json')} | ${'.'} | ${false} | ${['src/*.json']} + `( + 'normalizeGlobsToRoot exclude: $exclude; "$glob" -> "$root" = "$expectedGlobs"', + ({ glob, root, exclude, expectedGlobs }) => { + root = path.resolve(root); + const r = normalizeGlobsToRoot([glob], root, exclude); + expect(r).toEqual(expectedGlobs); + }, + ); + interface TestMapGlobToRoot { glob: string; globRoot: string; @@ -202,19 +234,17 @@ describe('Validate internal functions', () => { test.each` glob | globRoot | root | expectedGlobs | file | expectedToMatch - ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${[]} | ${''} | ${false} ${'**'} | ${'.'} | ${'.'} | ${['**']} | ${'./package.json'} | ${true} ${'*.json'} | ${'.'} | ${'.'} | ${['**/*.json', '**/*.json/**']} | ${'./package.json'} | ${true} ${'*.json'} | ${'.'} | ${'.'} | ${['**/*.json', '**/*.json/**']} | ${'./.git/package.json'} | ${true} ${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/*.json', 'project/p1/**/*.json/**']} | ${'./project/p1/package.json'} | ${true} ${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/*.json', 'project/p1/**/*.json/**']} | ${'./project/p1/src/package.json'} | ${true} ${'*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json', '**/*.json/**']} | ${'./project/p2/package.json'} | ${true} - ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${[]} | ${''} | ${false} ${'**/src/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/src/*.json', '**/src/*.json/**']} | ${'./project/p2/x/src/config.json'} | ${true} ${'**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['**/src/*.json', '**/src/*.json/**']} | ${'./project/p1/src/config.json'} | ${true} ${'/**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/src/*.json', 'project/p1/**/src/*.json/**']} | ${'./project/p1/src/config.json'} | ${true} `( - 'mapGlobToRoot exclude "$glob"@"$globRoot" -> "$root" = "$expectedGlobs"', + 'mapGlobToRoot exclude "$glob"@"$globRoot" -> "$root" = "$expectedGlobs" $file', ({ glob, globRoot, root, expectedGlobs, file, expectedToMatch }: TestMapGlobToRoot) => { globRoot = path.resolve(globRoot); root = path.resolve(root); @@ -234,16 +264,40 @@ describe('Validate internal functions', () => { }, ); + test.each` + glob | globRoot | root | expectedGlobs | file | expectedToMatch + ${'src/*.js'} | ${'./p2'} | ${'./src'} | ${[]} | ${''} | ${false} + ${'src/*.js'} | ${'./p2'} | ${'./p2'} | ${['src/*.js', 'src/*.js/**']} | ${'./p2/src/code.js'} | ${true} + ${'src/*.js'} | ${'.'} | ${'./src'} | ${['*.js', '*.js/**']} | ${'./src/code.js'} | ${true} + ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${['../../src/*.json', '../../src/*.json/**']} | ${'./src/data.json'} | ${true} + `( + 'mapGlobToRoot exclude "$glob"@"$globRoot" -> "$root" = "$expectedGlobs" $file', + ({ glob, globRoot, root, expectedGlobs, file, expectedToMatch }: TestMapGlobToRoot) => { + globRoot = path.resolve(globRoot); + root = path.resolve(root); + file = path.resolve(file); + const globMatcher = new GlobMatcher(glob, { + root: globRoot, + mode: 'exclude', + }); + const patterns = globMatcher.patterns.map((g) => g as Glob); + const r = normalizeGlobsToRoot(patterns, root, true); + expect(r).toEqual(expectedGlobs); + + expect(globMatcher.match(file)).toBe(expectedToMatch); + }, + ); + test.each` glob | globRoot | root | expectedGlobs | file | expectedToMatch ${'*.json'} | ${'.'} | ${'.'} | ${['*.json']} | ${'./package.json'} | ${true} ${'*.json'} | ${'.'} | ${'.'} | ${['*.json']} | ${'./.git/package.json'} | ${false} ${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/*.json']} | ${'./project/p1/package.json'} | ${true} ${'*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/*.json']} | ${'./project/p1/src/package.json'} | ${false} - ${'*.json'} | ${'.'} | ${'./project/p2'} | ${[]} | ${'./project/p2/package.json'} | ${false} + ${'*.json'} | ${'.'} | ${'./project/p2'} | ${['../../*.json']} | ${'./package.json'} | ${true} ${'/**/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json']} | ${'./project/p2/package.json'} | ${true} ${'**/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/*.json']} | ${'./project/p2/package.json'} | ${true} - ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${[]} | ${''} | ${false} + ${'src/*.json'} | ${'.'} | ${'./project/p2'} | ${['../../src/*.json']} | ${'./src/data.json'} | ${true} ${'**/src/*.json'} | ${'.'} | ${'./project/p2'} | ${['**/src/*.json']} | ${'./project/p2/x/src/config.json'} | ${true} ${'**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['**/src/*.json']} | ${'./project/p1/src/config.json'} | ${true} ${'/**/src/*.json'} | ${'./project/p1'} | ${'.'} | ${['project/p1/**/src/*.json']} | ${'./project/p1/src/config.json'} | ${true}