Skip to content

Commit 3ffc99e

Browse files
seanpoultercpojer
authored andcommitted
Resolve #5216 (#5269)
PR #5054 added a call to `replacePathSepForRegex` to escape values of the `--testPathPattern` and `<regexForTestFiles>` CLI options. Since the Windows path separator and the regular expression special character delimeter are the same character, this can lead to ambiguous patterns (e.g.: `app\book\d*\`). This commit: - Removes escaping CLI args with `replacePathSepForRegex` to leave them as is unless it's a POSIX path separator on Windows - Changes the tests in `normalize.test.js` to run the same test suite for `--testPathPattern` and `<regexForTestFiles>` - Reverts the changes to `replacePathSepForRegex` from #5230 but keeps the tests for the intended behavior. It will be complicated to escape the "safe" cases when `\` is a path separator and not a regular expression delimeter. Instead of getting fancy, we can urge Windows users to use `/` or `\\` as a path separator.
1 parent e95ec14 commit 3ffc99e

File tree

5 files changed

+94
-166
lines changed

5 files changed

+94
-166
lines changed

packages/jest-config/src/__tests__/normalize.test.js

Lines changed: 69 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,15 +1031,6 @@ describe('testPathPattern', () => {
10311031
const initialOptions = {rootDir: '/root'};
10321032
const consoleLog = console.log;
10331033

1034-
function testWindowsPathSeparator(argv, expected) {
1035-
jest.resetModules();
1036-
jest.mock('path', () => require.requireActual('path').win32);
1037-
require('jest-resolve').findNodeModule = findNodeModule;
1038-
1039-
const {options} = require('../normalize').default(initialOptions, argv);
1040-
expect(options.testPathPattern).toBe(expected);
1041-
}
1042-
10431034
beforeEach(() => {
10441035
console.log = jest.fn();
10451036
});
@@ -1053,62 +1044,85 @@ describe('testPathPattern', () => {
10531044
expect(options.testPathPattern).toBe('');
10541045
});
10551046

1056-
describe('--testPathPattern', () => {
1057-
it('uses testPathPattern if set', () => {
1058-
const {options} = normalize(initialOptions, {testPathPattern: ['a/b']});
1059-
expect(options.testPathPattern).toBe('a/b');
1060-
});
1047+
const cliOptions = [
1048+
{name: '--testPathPattern', property: 'testPathPattern'},
1049+
{name: '<regexForTestFiles>', property: '_'},
1050+
];
1051+
for (const opt of cliOptions) {
1052+
describe(opt.name, () => {
1053+
it('uses ' + opt.name + ' if set', () => {
1054+
const argv = {[opt.property]: ['a/b']};
1055+
const {options} = normalize(initialOptions, argv);
10611056

1062-
it('ignores invalid regular expressions and logs a warning', () => {
1063-
const {options} = normalize(initialOptions, {testPathPattern: ['a(']});
1064-
expect(options.testPathPattern).toBe('');
1065-
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
1066-
});
1057+
expect(options.testPathPattern).toBe('a/b');
1058+
});
10671059

1068-
it('escapes Windows path separators', () => {
1069-
testWindowsPathSeparator({testPathPattern: ['a\\b']}, 'a\\\\b');
1070-
});
1060+
it('ignores invalid regular expressions and logs a warning', () => {
1061+
const argv = {[opt.property]: ['a(']};
1062+
const {options} = normalize(initialOptions, argv);
10711063

1072-
it('joins multiple --testPathPatterns if set', () => {
1073-
const {options} = normalize(initialOptions, {
1074-
testPathPattern: ['a/b', 'c/d'],
1064+
expect(options.testPathPattern).toBe('');
1065+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
10751066
});
1076-
expect(options.testPathPattern).toBe('a/b|c/d');
1077-
});
10781067

1079-
it('escapes Windows path separators in multiple args', () => {
1080-
testWindowsPathSeparator(
1081-
{testPathPattern: ['a\\b', 'c\\d']},
1082-
'a\\\\b|c\\\\d',
1083-
);
1084-
});
1085-
});
1068+
it('joins multiple ' + opt.name + ' if set', () => {
1069+
const argv = {testPathPattern: ['a/b', 'c/d']};
1070+
const {options} = normalize(initialOptions, argv);
10861071

1087-
describe('<regexForTestFiles>', () => {
1088-
it('uses <regexForTestFiles> if set', () => {
1089-
const {options} = normalize(initialOptions, {_: ['a/b']});
1090-
expect(options.testPathPattern).toBe('a/b');
1091-
});
1092-
1093-
it('ignores invalid regular expressions and logs a warning', () => {
1094-
const {options} = normalize(initialOptions, {_: ['a(']});
1095-
expect(options.testPathPattern).toBe('');
1096-
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
1097-
});
1072+
expect(options.testPathPattern).toBe('a/b|c/d');
1073+
});
10981074

1099-
it('escapes Windows path separators', () => {
1100-
testWindowsPathSeparator({_: ['a\\b']}, 'a\\\\b');
1101-
});
1075+
describe('posix', () => {
1076+
it('should not escape the pattern', () => {
1077+
const argv = {[opt.property]: ['a\\/b', 'a/b', 'a\\b', 'a\\\\b']};
1078+
const {options} = normalize(initialOptions, argv);
11021079

1103-
it('joins multiple <regexForTestFiles> if set', () => {
1104-
const {options} = normalize(initialOptions, {_: ['a/b', 'c/d']});
1105-
expect(options.testPathPattern).toBe('a/b|c/d');
1106-
});
1080+
expect(options.testPathPattern).toBe('a\\/b|a/b|a\\b|a\\\\b');
1081+
});
1082+
});
11071083

1108-
it('escapes Windows path separators in multiple args', () => {
1109-
testWindowsPathSeparator({_: ['a\\b', 'c\\d']}, 'a\\\\b|c\\\\d');
1084+
describe('win32', () => {
1085+
beforeEach(() => {
1086+
jest.mock('path', () => require.requireActual('path').win32);
1087+
require('jest-resolve').findNodeModule = findNodeModule;
1088+
});
1089+
1090+
afterEach(() => {
1091+
jest.resetModules();
1092+
});
1093+
1094+
it('preserves any use of "\\"', () => {
1095+
const argv = {[opt.property]: ['a\\b', 'c\\\\d']};
1096+
const {options} = require('../normalize').default(
1097+
initialOptions,
1098+
argv,
1099+
);
1100+
1101+
expect(options.testPathPattern).toBe('a\\b|c\\\\d');
1102+
});
1103+
1104+
it('replaces POSIX path separators', () => {
1105+
const argv = {[opt.property]: ['a/b']};
1106+
const {options} = require('../normalize').default(
1107+
initialOptions,
1108+
argv,
1109+
);
1110+
1111+
expect(options.testPathPattern).toBe('a\\\\b');
1112+
});
1113+
1114+
it('replaces POSIX paths in multiple args', () => {
1115+
const argv = {[opt.property]: ['a/b', 'c/d']};
1116+
const {options} = require('../normalize').default(
1117+
initialOptions,
1118+
argv,
1119+
);
1120+
1121+
expect(options.testPathPattern).toBe('a\\\\b|c\\\\d');
1122+
});
1123+
});
11101124
});
1111-
});
1125+
}
11121126

11131127
it('joins multiple --testPathPatterns and <regexForTestFiles>', () => {
11141128
const {options} = normalize(initialOptions, {

packages/jest-config/src/normalize.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,14 @@ const buildTestPathPattern = (argv: Argv): string => {
289289
patterns.push(...argv.testPathPattern);
290290
}
291291

292-
const testPathPattern = patterns.map(replacePathSepForRegex).join('|');
292+
const replacePosixSep = (pattern: string) => {
293+
if (path.sep === '/') {
294+
return pattern;
295+
}
296+
return pattern.replace(/\//g, '\\\\');
297+
};
298+
299+
const testPathPattern = patterns.map(replacePosixSep).join('|');
293300
if (validatePattern(testPathPattern)) {
294301
return testPathPattern;
295302
} else {

packages/jest-regex-util/src/__tests__/__snapshots__/index.test.js.snap

Lines changed: 0 additions & 19 deletions
This file was deleted.

packages/jest-regex-util/src/__tests__/index.test.js

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,66 +4,40 @@ import {replacePathSepForRegex} from '../index';
44
import path from 'path';
55

66
describe('replacePathSepForRegex()', () => {
7-
const testPatternsFrom5216 = [
8-
'jest-config\\/.*normalize',
9-
'jest-config/.*normalize',
10-
'jest-config\\.*normalize',
11-
'jest-config\\\\.*normalize',
12-
];
13-
147
describe('posix', () => {
158
beforeEach(() => (path.sep = '/'));
169

1710
it('should return the path', () => {
1811
const expected = {};
1912
expect(replacePathSepForRegex(expected)).toBe(expected);
2013
});
21-
22-
// Confirming existing behavior; could be changed to improve cross-platform support
23-
it('should not replace Windows path separators', () => {
24-
expect(replacePathSepForRegex('a\\.*b')).toBe('a\\.*b');
25-
expect(replacePathSepForRegex('a\\\\.*b')).toBe('a\\\\.*b');
26-
});
27-
28-
// Bonus: Test cases from https://github.com/facebook/jest#5216
29-
it('should match the expected output from #5216', () => {
30-
expect(
31-
testPatternsFrom5216.map(replacePathSepForRegex),
32-
).toMatchSnapshot();
33-
});
3414
});
3515

3616
describe('win32', () => {
3717
beforeEach(() => (path.sep = '\\'));
3818

39-
it('should escape Windows path separators', () => {
40-
expect(replacePathSepForRegex('a\\b\\c')).toBe('a\\\\b\\\\c');
41-
});
42-
4319
it('should replace POSIX path separators', () => {
4420
expect(replacePathSepForRegex('a/b/c')).toBe('a\\\\b\\\\c');
45-
});
4621

47-
it('should not escape an escaped period', () => {
48-
expect(replacePathSepForRegex('a\\.dotfile')).toBe('a\\.dotfile');
49-
expect(replacePathSepForRegex('a\\\\\\.dotfile')).toBe('a\\\\\\.dotfile');
22+
// When a regular expression is created with a string, not enclosing
23+
// slashes like "/<pattern>/", the "/" character does not need to be
24+
// escaped as "\/". The result is the double path separator: "\\"
25+
expect(replacePathSepForRegex('a\\/b')).toBe('a\\\\\\\\b');
5026
});
5127

52-
it('should not escape an escaped Windows path separator', () => {
53-
expect(replacePathSepForRegex('a\\\\b')).toBe('a\\\\b');
54-
expect(replacePathSepForRegex('a\\\\.dotfile')).toBe('a\\\\.dotfile');
28+
it('should escape Windows path separators', () => {
29+
expect(replacePathSepForRegex('a\\b\\c')).toBe('a\\\\b\\\\c');
5530
});
5631

57-
// Confirming existing behavior; could be changed to improve cross-platform support
58-
it('should not replace escaped POSIX separators', () => {
59-
expect(replacePathSepForRegex('a\\/b')).toBe('a\\\\\\\\b');
60-
});
32+
it('should not escape an escaped dot', () => {
33+
expect(replacePathSepForRegex('a\\.dotfile')).toBe('a\\.dotfile');
6134

62-
// Bonus: Test cases from https://github.com/facebook/jest#5216
63-
it('should match the expected output from #5216', () => {
64-
expect(
65-
testPatternsFrom5216.map(replacePathSepForRegex),
66-
).toMatchSnapshot();
35+
// If we expect Windows path separators to be escaped, one would expect
36+
// the regular expression "\\\." to be unescaped as "\.". This is not the
37+
// current behavior.
38+
expect(replacePathSepForRegex('a\\\\\\.dotfile')).toBe(
39+
'a\\\\\\\\\\.dotfile',
40+
);
6741
});
6842
});
6943
});

packages/jest-regex-util/src/index.js

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,56 +22,8 @@ export const escapeStrForRegex = (string: string) =>
2222
string.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&');
2323

2424
export const replacePathSepForRegex = (string: string) => {
25-
if (!string || path.sep !== '\\') {
26-
return string;
27-
}
28-
29-
let result = '';
30-
for (let i = 0; i < string.length; i += 1) {
31-
const char = string[i];
32-
if (char === '\\') {
33-
const nextChar = string[i + 1];
34-
/* Case: \/ -- recreate legacy behavior */
35-
if (nextChar === '/') {
36-
i += 1;
37-
result += '\\\\\\\\';
38-
continue;
39-
}
40-
41-
/* Case: \. */
42-
if (nextChar === '.') {
43-
i += 1;
44-
result += '\\.';
45-
continue;
46-
}
47-
48-
/* Case: \\. */
49-
if (nextChar === '\\' && string[i + 2] === '.') {
50-
i += 2;
51-
result += '\\\\.';
52-
continue;
53-
}
54-
55-
/* Case: \\ */
56-
if (nextChar === '\\') {
57-
i += 1;
58-
result += '\\\\';
59-
continue;
60-
}
61-
62-
/* Case: \<other> */
63-
result += '\\\\';
64-
continue;
65-
}
66-
67-
/* Case: / */
68-
if (char === '/') {
69-
result += '\\\\';
70-
continue;
71-
}
72-
73-
result += char;
25+
if (path.sep === '\\') {
26+
return string.replace(/(\/|\\(?!\.))/g, '\\\\');
7427
}
75-
76-
return result;
28+
return string;
7729
};

0 commit comments

Comments
 (0)