diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfa03859ae9..b4b9f192770a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ - `[expect]` Fix toMatchObject matcher when used with `Object.create(null)` ([#7334](https://github.com/facebook/jest/pull/7334)) - `[jest-haste-map]` [**BREAKING**] Recover files correctly after haste name collisions are fixed ([#7329](https://github.com/facebook/jest/pull/7329)) - `[jest-haste-map]` Remove legacy condition for duplicate module detection ([#7333](https://github.com/facebook/jest/pull/7333)) +- `[jest-haste-map]` Fix `require` detection with trailing commas and ignore `import typeof` modules ([#7385](https://github.com/facebook/jest/pull/7385)) ### Chore & Maintenance @@ -110,6 +111,7 @@ - `[jest-worker]` Standardize filenames ([#7316](https://github.com/facebook/jest/pull/7316)) - `[pretty-format]` Standardize filenames ([#7316](https://github.com/facebook/jest/pull/7316)) - `[*]` Add check for Facebook copyright headers on CI ([#7370](https://github.com/facebook/jest/pull/7370)) +- `[jest-haste-map]` Refactor `dependencyExtractor` and tests ([#7385](https://github.com/facebook/jest/pull/7385)) ### Performance diff --git a/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.js b/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.js deleted file mode 100644 index 085b0f6f4c21..000000000000 --- a/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ -'use strict'; - -import {extract as extractRequires} from '../dependencyExtractor'; - -it('extracts both requires and imports from code', () => { - const code = ` - import module1 from 'module1'; - const module2 = require('module2'); - import('module3').then(module3 => {})'; - `; - - expect(extractRequires(code)).toEqual( - new Set(['module1', 'module2', 'module3']), - ); -}); - -it('extracts requires in order', () => { - const code = ` - const module1 = require('module1'); - const module2 = require('module2'); - const module3 = require('module3'); - `; - - expect(extractRequires(code)).toEqual( - new Set(['module1', 'module2', 'module3']), - ); -}); - -it('strips out comments from code', () => { - const code = `// comment const module2 = require('module2');`; - - expect(extractRequires(code)).toEqual(new Set([])); -}); - -it('ignores requires in comments', () => { - const code = [ - '// const module1 = require("module1");', - '/**', - ' * const module2 = require("module2");', - ' */', - ].join('\n'); - - expect(extractRequires(code)).toEqual(new Set([])); -}); - -it('ignores requires in comments with Windows line endings', () => { - const code = [ - '// const module1 = require("module1");', - '/**', - ' * const module2 = require("module2");', - ' */', - ].join('\r\n'); - - expect(extractRequires(code)).toEqual(new Set([])); -}); - -it('ignores requires in comments with unicode line endings', () => { - const code = [ - '// const module1 = require("module1");\u2028', - '// const module1 = require("module2");\u2029', - '/*\u2028', - 'const module2 = require("module3");\u2029', - ' */', - ].join(''); - - expect(extractRequires(code)).toEqual(new Set([])); -}); - -it('does not contain duplicates', () => { - const code = ` - const module1 = require('module1'); - const module1Dup = require('module1'); - `; - - expect(extractRequires(code)).toEqual(new Set(['module1'])); -}); - -it('ignores type imports', () => { - const code = [ - "import type foo from 'bar';", - 'import type {', - ' bar,', - ' baz,', - "} from 'wham'", - ].join('\r\n'); - - expect(extractRequires(code)).toEqual(new Set([])); -}); - -it('ignores type exports', () => { - const code = [ - 'export type Foo = number;', - 'export default {}', - "export * from 'module1'", - ].join('\r\n'); - - expect(extractRequires(code)).toEqual(new Set(['module1'])); -}); - -it('understands require.requireActual', () => { - const code = `require.requireActual('pizza');`; - expect(extractRequires(code)).toEqual(new Set(['pizza'])); -}); - -it('understands jest.requireActual', () => { - const code = `jest.requireActual('whiskey');`; - expect(extractRequires(code)).toEqual(new Set(['whiskey'])); -}); - -it('understands require.requireMock', () => { - const code = `require.requireMock('cheeseburger');`; - expect(extractRequires(code)).toEqual(new Set(['cheeseburger'])); -}); - -it('understands jest.requireMock', () => { - const code = `jest.requireMock('scotch');`; - expect(extractRequires(code)).toEqual(new Set(['scotch'])); -}); diff --git a/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.test.js b/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.test.js new file mode 100644 index 000000000000..7d10e43b2ea7 --- /dev/null +++ b/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.test.js @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {extract} from '../dependencyExtractor'; +import isRegExpSupported from '../isRegExpSupported'; + +const COMMENT_NO_NEG_LB = isRegExpSupported('(? { + it('should not extract dependencies inside comments', () => { + const code = ` + // import a from 'ignore-line-comment'; + // require('ignore-line-comment'); + /* + * import a from 'ignore-block-comment'; + * require('ignore-block-comment'); + */ + `; + expect(extract(code)).toEqual(new Set()); + }); + + it('should not extract dependencies inside comments (windows line endings)', () => { + const code = [ + '// const module1 = require("module1");', + '/**', + ' * const module2 = require("module2");', + ' */', + ].join('\r\n'); + + expect(extract(code)).toEqual(new Set([])); + }); + + it('should not extract dependencies inside comments (unicode line endings)', () => { + const code = [ + '// const module1 = require("module1");\u2028', + '// const module1 = require("module2");\u2029', + '/*\u2028', + 'const module2 = require("module3");\u2029', + ' */', + ].join(''); + + expect(extract(code)).toEqual(new Set([])); + }); + + it('should extract dependencies from `import` statements', () => { + const code = ` + // Good + import * as depNS from 'dep1'; + import { + a as aliased_a, + b, + } from 'dep2'; + import depDefault from 'dep3'; + import * as depNS, { + a as aliased_a, + b, + }, depDefault from 'dep4'; + + // Bad + ${COMMENT_NO_NEG_LB} foo . import ('inv1'); + ${COMMENT_NO_NEG_LB} foo . export ('inv2'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + }); + + it('should not extract dependencies from `import type/typeof` statements', () => { + const code = ` + // Bad + import typeof {foo} from 'inv1'; + import type {foo} from 'inv2'; + `; + expect(extract(code)).toEqual(new Set([])); + }); + + it('should extract dependencies from `export` statements', () => { + const code = ` + // Good + export * as depNS from 'dep1'; + export { + a as aliased_a, + b, + } from 'dep2'; + export depDefault from 'dep3'; + export * as depNS, { + a as aliased_a, + b, + }, depDefault from 'dep4'; + + // Bad + ${COMMENT_NO_NEG_LB} foo . export ('inv1'); + ${COMMENT_NO_NEG_LB} foo . export ('inv2'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + }); + + it('should extract dependencies from `export-from` statements', () => { + const code = ` + // Good + export * as depNS from 'dep1'; + export { + a as aliased_a, + b, + } from 'dep2'; + export depDefault from 'dep3'; + export * as depNS, { + a as aliased_a, + b, + }, depDefault from 'dep4'; + + // Bad + ${COMMENT_NO_NEG_LB} foo . export ('inv1'); + ${COMMENT_NO_NEG_LB} foo . export ('inv2'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + }); + + it('should not extract dependencies from `export type/typeof` statements', () => { + const code = ` + // Bad + export typeof {foo} from 'inv1'; + export type {foo} from 'inv2'; + `; + expect(extract(code)).toEqual(new Set([])); + }); + + it('should extract dependencies from dynamic `import` calls', () => { + const code = ` + // Good + import('dep1').then(); + const dep2 = await import( + "dep2", + ); + if (await import(\`dep3\`)) {} + + // Bad + ${COMMENT_NO_NEG_LB} await foo . import('inv1') + await ximport('inv2'); + importx('inv3'); + import('inv4', 'inv5'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3'])); + }); + + it('should extract dependencies from `require` calls', () => { + const code = ` + // Good + require('dep1'); + const dep2 = require( + "dep2", + ); + if (require(\`dep3\`).cond) {} + + // Bad + ${COMMENT_NO_NEG_LB} foo . require('inv1') + xrequire('inv2'); + requirex('inv3'); + require('inv4', 'inv5'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3'])); + }); + + it('should extract dependencies from `require.requireActual` calls', () => { + const code = ` + // Good + require.requireActual('dep1'); + const dep2 = require.requireActual( + "dep2", + ); + if (require.requireActual(\`dep3\`).cond) {} + require + .requireActual('dep4'); + + // Bad + ${COMMENT_NO_NEG_LB} foo . require.requireActual('inv1') + xrequire.requireActual('inv2'); + require.requireActualx('inv3'); + require.requireActual('inv4', 'inv5'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + }); + + it('should extract dependencies from `require.requireMock` calls', () => { + const code = ` + // Good + require.requireMock('dep1'); + const dep2 = require.requireMock( + "dep2", + ); + if (require.requireMock(\`dep3\`).cond) {} + require + .requireMock('dep4'); + + // Bad + ${COMMENT_NO_NEG_LB} foo . require.requireMock('inv1') + xrequire.requireMock('inv2'); + require.requireMockx('inv3'); + require.requireMock('inv4', 'inv5'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + }); + + it('should extract dependencies from `jest.requireActual` calls', () => { + const code = ` + // Good + jest.requireActual('dep1'); + const dep2 = jest.requireActual( + "dep2", + ); + if (jest.requireActual(\`dep3\`).cond) {} + require + .requireActual('dep4'); + + // Bad + ${COMMENT_NO_NEG_LB} foo . jest.requireActual('inv1') + xjest.requireActual('inv2'); + jest.requireActualx('inv3'); + jest.requireActual('inv4', 'inv5'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + }); + + it('should extract dependencies from `jest.requireMock` calls', () => { + const code = ` + // Good + jest.requireMock('dep1'); + const dep2 = jest.requireMock( + "dep2", + ); + if (jest.requireMock(\`dep3\`).cond) {} + require + .requireMock('dep4'); + + // Bad + ${COMMENT_NO_NEG_LB} foo . jest.requireMock('inv1') + xjest.requireMock('inv2'); + jest.requireMockx('inv3'); + jest.requireMock('inv4', 'inv5'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + }); + + it('should extract dependencies from `jest.genMockFromModule` calls', () => { + const code = ` + // Good + jest.genMockFromModule('dep1'); + const dep2 = jest.genMockFromModule( + "dep2", + ); + if (jest.genMockFromModule(\`dep3\`).cond) {} + require + .requireMock('dep4'); + + // Bad + ${COMMENT_NO_NEG_LB} foo . jest.genMockFromModule('inv1') + xjest.genMockFromModule('inv2'); + jest.genMockFromModulex('inv3'); + jest.genMockFromModule('inv4', 'inv5'); + `; + expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + }); +}); diff --git a/packages/jest-haste-map/src/lib/__tests__/isRegExpSupported.test.js b/packages/jest-haste-map/src/lib/__tests__/isRegExpSupported.test.js new file mode 100644 index 000000000000..5408244073c9 --- /dev/null +++ b/packages/jest-haste-map/src/lib/__tests__/isRegExpSupported.test.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import isRegExpSupported from '../isRegExpSupported'; + +describe('isRegExpSupported', () => { + it('should return true when passing valid regular expression', () => { + expect(isRegExpSupported('(?:foo|bar)')).toBe(true); + }); + + it('should return false when passing an invalid regular expression', () => { + expect(isRegExpSupported('(?_foo|bar)')).toBe(false); + }); +}); diff --git a/packages/jest-haste-map/src/lib/dependencyExtractor.js b/packages/jest-haste-map/src/lib/dependencyExtractor.js index c41393b2bb85..038e6084c2ce 100644 --- a/packages/jest-haste-map/src/lib/dependencyExtractor.js +++ b/packages/jest-haste-map/src/lib/dependencyExtractor.js @@ -7,32 +7,88 @@ * @flow */ -const blockCommentRe = /\/\*[^]*?\*\//g; -const lineCommentRe = /\/\/.*/g; +import isRegExpSupported from './isRegExpSupported'; -const replacePatterns = { - DYNAMIC_IMPORT_RE: /(?:^|[^.]\s*)(\bimport\s*?\(\s*?)([`'"])([^`'"]+)(\2\s*?\))/g, - EXPORT_RE: /(\bexport\s+(?!type )(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g, - IMPORT_RE: /(\bimport\s+(?!type )(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g, - REQUIRE_EXTENSIONS_PATTERN: /(?:^|[^.]\s*)(\b(?:require\s*?\.\s*?(?:requireActual|requireMock)|jest\s*?\.\s*?(?:requireActual|requireMock|genMockFromModule))\s*?\(\s*?)([`'"])([^`'"]+)(\2\s*?\))/g, - REQUIRE_RE: /(?:^|[^.]\s*)(\brequire\s*?\(\s*?)([`'"])([^`'"]+)(\2\s*?\))/g, -}; +// Negative look behind is only supported in Node 9+ +const NOT_A_DOT = isRegExpSupported('(? `([\`'"])([^'"\`]*?)(?:\\${pos})`; +const WORD_SEPARATOR = '\\b'; +const LEFT_PARENTHESIS = '\\('; +const RIGHT_PARENTHESIS = '\\)'; +const WHITESPACE = '\\s*'; +const OPTIONAL_COMMA = '(:?,\\s*)?'; + +function createRegExp(parts, flags) { + return new RegExp(parts.join(''), flags); +} + +function alternatives(...parts) { + return `(?:${parts.join('|')})`; +} + +function functionCallStart(...names) { + return [ + NOT_A_DOT, + WORD_SEPARATOR, + alternatives(...names), + WHITESPACE, + LEFT_PARENTHESIS, + WHITESPACE, + ]; +} + +const BLOCK_COMMENT_RE = /\/\*[^]*?\*\//g; +const LINE_COMMENT_RE = /\/\/.*/g; + +const REQUIRE_OR_DYNAMIC_IMPORT_RE = createRegExp( + [ + ...functionCallStart('require', 'import'), + CAPTURE_STRING_LITERAL(1), + WHITESPACE, + OPTIONAL_COMMA, + RIGHT_PARENTHESIS, + ], + 'g', +); + +const IMPORT_OR_EXPORT_RE = createRegExp( + [ + '\\b(?:import|export)\\s+(?!type(?:of)?\\s+)[^\'"]+\\s+from\\s+', + CAPTURE_STRING_LITERAL(1), + ], + 'g', +); + +const JEST_EXTENSIONS_RE = createRegExp( + [ + ...functionCallStart( + 'require\\s*\\.\\s*(?:requireActual|requireMock)', + 'jest\\s*\\.\\s*(?:requireActual|requireMock|genMockFromModule)', + ), + CAPTURE_STRING_LITERAL(1), + WHITESPACE, + OPTIONAL_COMMA, + RIGHT_PARENTHESIS, + ], + 'g', +); export function extract(code: string): Set { const dependencies = new Set(); - const addDependency = (match, pre, quot, dep, post) => { + + const addDependency = (match: string, q: string, dep: string) => { dependencies.add(dep); return match; }; code - .replace(blockCommentRe, '') - .replace(lineCommentRe, '') - .replace(replacePatterns.EXPORT_RE, addDependency) - .replace(replacePatterns.IMPORT_RE, addDependency) - .replace(replacePatterns.REQUIRE_EXTENSIONS_PATTERN, addDependency) - .replace(replacePatterns.REQUIRE_RE, addDependency) - .replace(replacePatterns.DYNAMIC_IMPORT_RE, addDependency); + .replace(BLOCK_COMMENT_RE, '') + .replace(LINE_COMMENT_RE, '') + .replace(IMPORT_OR_EXPORT_RE, addDependency) + .replace(REQUIRE_OR_DYNAMIC_IMPORT_RE, addDependency) + .replace(JEST_EXTENSIONS_RE, addDependency); return dependencies; } diff --git a/packages/jest-haste-map/src/lib/isRegExpSupported.js b/packages/jest-haste-map/src/lib/isRegExpSupported.js new file mode 100644 index 000000000000..d20cb1a6e21e --- /dev/null +++ b/packages/jest-haste-map/src/lib/isRegExpSupported.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export default function isRegExpSupported(value: string): boolean { + try { + // eslint-disable-next-line no-new + new RegExp(value); + return true; + } catch (e) { + return false; + } +}