From 1d32972ae4f03dce8f7ba5603b27a11a48859423 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 8 Nov 2025 16:46:36 +0700 Subject: [PATCH 01/14] feat(rule): add new rule to validate `jest.mock` path existence Signed-off-by: hainenber --- README.md | 1 + docs/rules/valid-mocked-module-path.md | 43 ++++++++++ src/rules/__tests__/fixtures/module/foo.js | 1 + src/rules/__tests__/fixtures/module/foo.ts | 1 + src/rules/__tests__/fixtures/module/index.ts | 1 + .../valid-mocked-module-path.test.ts | 60 +++++++++++++ src/rules/no-untyped-mock-factory.ts | 11 +-- src/rules/utils/misc.ts | 10 +++ src/rules/valid-mocked-module-path.ts | 85 +++++++++++++++++++ 9 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 docs/rules/valid-mocked-module-path.md create mode 100644 src/rules/__tests__/fixtures/module/foo.js create mode 100644 src/rules/__tests__/fixtures/module/foo.ts create mode 100644 src/rules/__tests__/fixtures/module/index.ts create mode 100644 src/rules/__tests__/valid-mocked-module-path.test.ts create mode 100644 src/rules/valid-mocked-module-path.ts diff --git a/README.md b/README.md index 79064c319..765a5ff95 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,7 @@ Manually fixable by | [valid-describe-callback](docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback | ✅ | | | | | [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | 🔧 | | | [valid-expect-in-promise](docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | | +| [valid-mocked-module-path](docs/rules/valid-mocked-module-path.md) | Disallow mocking of non-existing module path | | | | | | [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ✅ | | 🔧 | | ### Requires Type Checking diff --git a/docs/rules/valid-mocked-module-path.md b/docs/rules/valid-mocked-module-path.md new file mode 100644 index 000000000..8e19fdcb3 --- /dev/null +++ b/docs/rules/valid-mocked-module-path.md @@ -0,0 +1,43 @@ +# Disallow mocking of non-existing module path (`valid-mocked-module-path`) + + + +This rule raises an error when using `jest.mock` and `jest.doMock` and the first +argument for mocked object (module/local file) do not exist. + +## Rule details + +This rule checks existence of the supplied path for `jest.mock` or `jest.doMock` +in the first argument. + +The following patterns are considered errors: + +```js +// Module(s) that cannot be found +jest.mock('@org/some-module-not-in-package-json'); +jest.mock('some-module-not-in-package-json'); + +// Local module (directory) that cannot be found +jest.mock('../../this/module/does/not/exist'); + +// Local file that cannot be found +jest.mock('../../this/path/does/not/exist.js'); +``` + +The following patterns are **not** considered errors: + +```js +// Module(s) that can be found +jest.mock('@org/some-module-in-package-json'); +jest.mock('some-module-in-package-json'); + +// Local module that cannot be found +jest.mock('../../this/module/really/does/exist'); + +// Local file that cannot be found +jest.mock('../../this/path/really/does/exist.js'); +``` + +## When Not To Use It + +Don't use this rule on non-jest test files. diff --git a/src/rules/__tests__/fixtures/module/foo.js b/src/rules/__tests__/fixtures/module/foo.js new file mode 100644 index 000000000..b9627ca7e --- /dev/null +++ b/src/rules/__tests__/fixtures/module/foo.js @@ -0,0 +1 @@ +export const foo = 'foo_js'; \ No newline at end of file diff --git a/src/rules/__tests__/fixtures/module/foo.ts b/src/rules/__tests__/fixtures/module/foo.ts new file mode 100644 index 000000000..4221ba044 --- /dev/null +++ b/src/rules/__tests__/fixtures/module/foo.ts @@ -0,0 +1 @@ +export const foo = 'foo_ts'; \ No newline at end of file diff --git a/src/rules/__tests__/fixtures/module/index.ts b/src/rules/__tests__/fixtures/module/index.ts new file mode 100644 index 000000000..d6c3be183 --- /dev/null +++ b/src/rules/__tests__/fixtures/module/index.ts @@ -0,0 +1 @@ +export * from './foo'; diff --git a/src/rules/__tests__/valid-mocked-module-path.test.ts b/src/rules/__tests__/valid-mocked-module-path.test.ts new file mode 100644 index 000000000..40e23ce0f --- /dev/null +++ b/src/rules/__tests__/valid-mocked-module-path.test.ts @@ -0,0 +1,60 @@ +import rule from '../valid-mocked-module-path'; +import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils'; + +const ruleTester = new RuleTester({ + parser: espreeParser, + parserOptions: { + ecmaVersion: 2015, + }, +}); + +ruleTester.run('valid-mocked-module-path', rule, { + valid: [ + { filename: __filename, code: 'jest.mock("./fixtures/module")' }, + { filename: __filename, code: 'jest.doMock("./fixtures/module")' }, + { filename: __filename, code: 'jest.mock("./fixtures/module/foo.ts")' }, + { filename: __filename, code: 'jest.doMock("./fixtures/module/foo.ts")' }, + { filename: __filename, code: 'jest.mock("./fixtures/module/foo.js")' }, + { filename: __filename, code: 'jest.doMock("./fixtures/module/foo.js")' }, + 'jest.mock("dedent")', + 'jest.doMock("dedent")', + ], + invalid: [ + { + filename: __filename, + code: "jest.mock('../module/does/not/exist')", + errors: [ + { + messageId: 'invalidMockModulePath', + data: { moduleName: "'../module/does/not/exist'" }, + column: 1, + line: 1, + }, + ], + }, + { + filename: __filename, + code: 'jest.mock("../file/does/not/exist.ts")', + errors: [ + { + messageId: 'invalidMockModulePath', + data: { moduleName: '"../file/does/not/exist.ts"' }, + column: 1, + line: 1, + }, + ], + }, + { + filename: __filename, + code: 'jest.mock("@doesnotexist/module")', + errors: [ + { + messageId: 'invalidMockModulePath', + data: { moduleName: '"@doesnotexist/module"' }, + column: 1, + line: 1, + }, + ], + }, + ], +}); diff --git a/src/rules/no-untyped-mock-factory.ts b/src/rules/no-untyped-mock-factory.ts index 4f0448696..cebc26f33 100644 --- a/src/rules/no-untyped-mock-factory.ts +++ b/src/rules/no-untyped-mock-factory.ts @@ -1,22 +1,13 @@ import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; import { createRule, + findModuleName, getAccessorValue, isFunction, isSupportedAccessor, isTypeOfJestFnCall, } from './utils'; -const findModuleName = ( - node: TSESTree.Literal | TSESTree.Node, -): TSESTree.StringLiteral | null => { - if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') { - return node; - } - - return null; -}; - export default createRule({ name: __filename, meta: { diff --git a/src/rules/utils/misc.ts b/src/rules/utils/misc.ts index 71c8c91e1..fb56c85c8 100644 --- a/src/rules/utils/misc.ts +++ b/src/rules/utils/misc.ts @@ -225,3 +225,13 @@ export const getFirstMatcherArg = ( return followTypeAssertionChain(firstArg); }; + +export const findModuleName = ( + node: TSESTree.Literal | TSESTree.Node, +): TSESTree.StringLiteral | null => { + if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') { + return node; + } + + return null; +}; diff --git a/src/rules/valid-mocked-module-path.ts b/src/rules/valid-mocked-module-path.ts new file mode 100644 index 000000000..7a1404f9c --- /dev/null +++ b/src/rules/valid-mocked-module-path.ts @@ -0,0 +1,85 @@ +import { statSync } from 'fs'; +import path from 'path'; +import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; +import { + createRule, + findModuleName, + getAccessorValue, + isSupportedAccessor, + isTypeOfJestFnCall, +} from './utils'; + +export default createRule({ + name: __filename, + meta: { + type: 'problem', + docs: { + description: 'Disallow mocking of non-existing module path', + }, + messages: { + invalidMockModulePath: 'Mocked module path {{moduleName}} does not exist', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node: TSESTree.CallExpression): void { + const { callee } = node; + + if (callee.type !== AST_NODE_TYPES.MemberExpression) { + return; + } + + const { property } = callee; + + if ( + node.arguments.length >= 1 && + isTypeOfJestFnCall(node, context, ['jest']) && + isSupportedAccessor(property) && + ['mock', 'doMock'].includes(getAccessorValue(property)) + ) { + const [nameNode] = node.arguments; + const moduleName = findModuleName(nameNode); + + try { + if (moduleName) { + if (moduleName.value.startsWith('.')) { + const resolvedModulePath = path.resolve( + path.dirname(context.filename), + moduleName.value, + ); + + const hasPossiblyModulePaths = ['', '.js', '.ts'] + .map(ext => `${resolvedModulePath}${ext}`) + .some(modPath => { + try { + statSync(modPath); + + return true; + } catch { + return false; + } + }); + + if (!hasPossiblyModulePaths) { + throw { code: 'MODULE_NOT_FOUND' }; + } + } else { + require.resolve(moduleName.value); + } + } + } catch (err: any) { + if (err?.code === 'MODULE_NOT_FOUND' || err?.code === 'ENOENT') { + context.report({ + messageId: 'invalidMockModulePath', + data: { moduleName: moduleName?.raw ?? './module-name' }, + node, + }); + } + } + } + }, + }; + }, +}); From 9ce0a40fe90accce8215e91b9292cfe2aa3cb5cc Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 8 Nov 2025 17:01:03 +0700 Subject: [PATCH 02/14] chore: update rules.test.ts due to newly added rule Signed-off-by: hainenber --- src/__tests__/__snapshots__/rules.test.ts.snap | 2 ++ src/__tests__/rules.test.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index cbd7a4236..3333b477d 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -72,6 +72,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/valid-describe-callback": "error", "jest/valid-expect": "error", "jest/valid-expect-in-promise": "error", + "jest/valid-mocked-module-path": "error", "jest/valid-title": "error", }, }, @@ -164,6 +165,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/valid-describe-callback": "error", "jest/valid-expect": "error", "jest/valid-expect-in-promise": "error", + "jest/valid-mocked-module-path": "error", "jest/valid-title": "error", }, }, diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index 857f65a41..a1a9af556 100644 --- a/src/__tests__/rules.test.ts +++ b/src/__tests__/rules.test.ts @@ -2,7 +2,7 @@ import { existsSync } from 'fs'; import { resolve } from 'path'; import plugin from '../'; -const numberOfRules = 63; +const numberOfRules = 64; const ruleNames = Object.keys(plugin.rules); const deprecatedRules = Object.entries(plugin.rules) .filter(([, rule]) => rule.meta.deprecated) From 621b8da1a505a908196093f427c72f2a24f7c71a Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 8 Nov 2025 20:52:54 +0700 Subject: [PATCH 03/14] chore: rewrite implementation to return early when linting over unwanted LoCs Signed-off-by: hainenber --- .../valid-mocked-module-path.test.ts | 18 +++- src/rules/valid-mocked-module-path.ts | 84 +++++++++++-------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/rules/__tests__/valid-mocked-module-path.test.ts b/src/rules/__tests__/valid-mocked-module-path.test.ts index 40e23ce0f..509f3960a 100644 --- a/src/rules/__tests__/valid-mocked-module-path.test.ts +++ b/src/rules/__tests__/valid-mocked-module-path.test.ts @@ -1,3 +1,4 @@ +import dedent from 'dedent'; import rule from '../valid-mocked-module-path'; import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils'; @@ -11,13 +12,26 @@ const ruleTester = new RuleTester({ ruleTester.run('valid-mocked-module-path', rule, { valid: [ { filename: __filename, code: 'jest.mock("./fixtures/module")' }, + { filename: __filename, code: 'jest.mock("./fixtures/module", () => {})' }, + { filename: __filename, code: 'jest.mock()' }, + { + filename: __filename, + code: 'jest.doMock("./fixtures/module", () => {})', + }, + { + filename: __filename, + code: dedent` + describe("foo", () => {}); + `, + }, { filename: __filename, code: 'jest.doMock("./fixtures/module")' }, { filename: __filename, code: 'jest.mock("./fixtures/module/foo.ts")' }, { filename: __filename, code: 'jest.doMock("./fixtures/module/foo.ts")' }, { filename: __filename, code: 'jest.mock("./fixtures/module/foo.js")' }, { filename: __filename, code: 'jest.doMock("./fixtures/module/foo.js")' }, - 'jest.mock("dedent")', - 'jest.doMock("dedent")', + 'jest.mock("eslint")', + 'jest.doMock("eslint")', + 'jest.mock("child_process")', ], invalid: [ { diff --git a/src/rules/valid-mocked-module-path.ts b/src/rules/valid-mocked-module-path.ts index 7a1404f9c..5acf0d56a 100644 --- a/src/rules/valid-mocked-module-path.ts +++ b/src/rules/valid-mocked-module-path.ts @@ -34,50 +34,64 @@ export default createRule({ const { property } = callee; if ( - node.arguments.length >= 1 && - isTypeOfJestFnCall(node, context, ['jest']) && - isSupportedAccessor(property) && - ['mock', 'doMock'].includes(getAccessorValue(property)) + !node.arguments.length || + !isTypeOfJestFnCall(node, context, ['jest']) || + !( + isSupportedAccessor(property) && + ['mock', 'doMock'].includes(getAccessorValue(property)) + ) ) { - const [nameNode] = node.arguments; - const moduleName = findModuleName(nameNode); + return; + } - try { - if (moduleName) { - if (moduleName.value.startsWith('.')) { - const resolvedModulePath = path.resolve( - path.dirname(context.filename), - moduleName.value, - ); + const [nameNode] = node.arguments; + const moduleName = findModuleName(nameNode); + + /* istanbul ignore if */ + if (!moduleName) { + throw new Error( + 'Cannot parse mocked module name from `jest.mock` - - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`', + ); + } - const hasPossiblyModulePaths = ['', '.js', '.ts'] - .map(ext => `${resolvedModulePath}${ext}`) - .some(modPath => { - try { - statSync(modPath); + try { + if (moduleName.value.startsWith('.')) { + const resolvedModulePath = path.resolve( + path.dirname(context.filename), + moduleName.value, + ); - return true; - } catch { - return false; - } - }); + const hasPossiblyModulePaths = ['', '.js', '.ts'] + .map(ext => `${resolvedModulePath}${ext}`) + .some(modPath => { + try { + statSync(modPath); - if (!hasPossiblyModulePaths) { - throw { code: 'MODULE_NOT_FOUND' }; + return true; + } catch { + return false; } - } else { - require.resolve(moduleName.value); - } - } - } catch (err: any) { - if (err?.code === 'MODULE_NOT_FOUND' || err?.code === 'ENOENT') { - context.report({ - messageId: 'invalidMockModulePath', - data: { moduleName: moduleName?.raw ?? './module-name' }, - node, }); + + if (!hasPossiblyModulePaths) { + throw { code: 'MODULE_NOT_FOUND' }; } + } else { + require.resolve(moduleName.value); } + } catch (err: any) { + // Skip over any unexpected issues when attempt to verify mocked module path. + // The list of possible errors is non-exhaustive. + /* istanbul ignore if */ + if (!['MODULE_NOT_FOUND', 'ENOENT'].includes(err.code)) { + return; + } + + context.report({ + messageId: 'invalidMockModulePath', + data: { moduleName: moduleName.raw }, + node, + }); } }, }; From c7d19016b3ade0afad901bd1b677d332b0162f90 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 8 Nov 2025 21:16:36 +0700 Subject: [PATCH 04/14] fix(ci): verify broken npmjs link in MD file by checking its `registry.npmjs.org` URI instead Signed-off-by: hainenber --- markdown_link_check_config.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/markdown_link_check_config.json b/markdown_link_check_config.json index bb5e90455..2b15c894f 100644 --- a/markdown_link_check_config.json +++ b/markdown_link_check_config.json @@ -6,5 +6,11 @@ "Accept-Encoding": "br, gzip, deflate" } } + ], + "replacementPatterns": [ + { + "pattern": "^https:\\/\\/www\\.npmjs\\.com\\/package", + "replacement": "https://registry.npmjs.org/" + } ] } From fee7e7af6bebecd66ee8522b648ffbf950f83ea0 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 8 Nov 2025 21:23:36 +0700 Subject: [PATCH 05/14] build(dev-deps): update `markdown-link-check` Signed-off-by: hainenber --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index bf8f844bb..68d46fca8 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "jest": "^30.0.0", "jest-runner-eslint": "^2.0.0", "lint-staged": "^16.0.0", - "markdown-link-check": "^3.13.7", + "markdown-link-check": "^3.14.1", "pinst": "^3.0.0", "prettier": "^3.0.0", "rimraf": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index ff5077cc8..e80f446a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5463,7 +5463,7 @@ __metadata: jest: "npm:^30.0.0" jest-runner-eslint: "npm:^2.0.0" lint-staged: "npm:^16.0.0" - markdown-link-check: "npm:^3.13.7" + markdown-link-check: "npm:^3.14.1" pinst: "npm:^3.0.0" prettier: "npm:^3.0.0" rimraf: "npm:^6.0.0" @@ -8442,7 +8442,7 @@ __metadata: languageName: node linkType: hard -"markdown-link-check@npm:^3.13.7": +"markdown-link-check@npm:^3.14.1": version: 3.14.1 resolution: "markdown-link-check@npm:3.14.1" dependencies: From bf6127f993ebd8f5979bdb34ab56f13f85317283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=97=20Tr=E1=BB=8Dng=20H=E1=BA=A3i?= <41283691+hainenber@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:10:33 +0700 Subject: [PATCH 06/14] feat: apply suggestions from @G-Rath code review Co-authored-by: Gareth Jones <3151613+G-Rath@users.noreply.github.com> --- src/rules/valid-mocked-module-path.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rules/valid-mocked-module-path.ts b/src/rules/valid-mocked-module-path.ts index 5acf0d56a..c1cde56e4 100644 --- a/src/rules/valid-mocked-module-path.ts +++ b/src/rules/valid-mocked-module-path.ts @@ -17,7 +17,7 @@ export default createRule({ description: 'Disallow mocking of non-existing module path', }, messages: { - invalidMockModulePath: 'Mocked module path {{moduleName}} does not exist', + invalidMockModulePath: 'Module path {{ moduleName }} does not exist', }, schema: [], }, @@ -44,8 +44,7 @@ export default createRule({ return; } - const [nameNode] = node.arguments; - const moduleName = findModuleName(nameNode); + const moduleName = findModuleName(node.arguments[0]); /* istanbul ignore if */ if (!moduleName) { From 172594cab4f3cd66fbc0e9aea15006a82e1be2ef Mon Sep 17 00:00:00 2001 From: hainenber Date: Mon, 10 Nov 2025 21:11:56 +0700 Subject: [PATCH 07/14] Revert "fix(ci): verify broken npmjs link in MD file by checking its `registry.npmjs.org` URI instead" This reverts commit c7d19016b3ade0afad901bd1b677d332b0162f90. --- markdown_link_check_config.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/markdown_link_check_config.json b/markdown_link_check_config.json index 2b15c894f..bb5e90455 100644 --- a/markdown_link_check_config.json +++ b/markdown_link_check_config.json @@ -6,11 +6,5 @@ "Accept-Encoding": "br, gzip, deflate" } } - ], - "replacementPatterns": [ - { - "pattern": "^https:\\/\\/www\\.npmjs\\.com\\/package", - "replacement": "https://registry.npmjs.org/" - } ] } From 4fb268122ac17df24626da148a7bea2afb6d865e Mon Sep 17 00:00:00 2001 From: hainenber Date: Mon, 10 Nov 2025 21:12:14 +0700 Subject: [PATCH 08/14] Revert "build(dev-deps): update `markdown-link-check`" This reverts commit fee7e7af6bebecd66ee8522b648ffbf950f83ea0. --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 68d46fca8..bf8f844bb 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "jest": "^30.0.0", "jest-runner-eslint": "^2.0.0", "lint-staged": "^16.0.0", - "markdown-link-check": "^3.14.1", + "markdown-link-check": "^3.13.7", "pinst": "^3.0.0", "prettier": "^3.0.0", "rimraf": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index e80f446a5..ff5077cc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5463,7 +5463,7 @@ __metadata: jest: "npm:^30.0.0" jest-runner-eslint: "npm:^2.0.0" lint-staged: "npm:^16.0.0" - markdown-link-check: "npm:^3.14.1" + markdown-link-check: "npm:^3.13.7" pinst: "npm:^3.0.0" prettier: "npm:^3.0.0" rimraf: "npm:^6.0.0" @@ -8442,7 +8442,7 @@ __metadata: languageName: node linkType: hard -"markdown-link-check@npm:^3.14.1": +"markdown-link-check@npm:^3.13.7": version: 3.14.1 resolution: "markdown-link-check@npm:3.14.1" dependencies: From 80063e60b96d6d4273220927c987db2134cebdf1 Mon Sep 17 00:00:00 2001 From: hainenber Date: Tue, 11 Nov 2025 22:13:28 +0700 Subject: [PATCH 09/14] feat: change rule name + add tests + configurable module file extensions + faster return branches Signed-off-by: hainenber --- README.md | 2 +- ...dule-path.md => valid-mock-module-path.md} | 32 ++++- .../__snapshots__/rules.test.ts.snap | 4 +- .../__tests__/fixtures/module/jsx/foo.jsx | 0 .../__tests__/fixtures/module/tsx/foo.jsx | 0 ...test.ts => valid-mock-module-path.test.ts} | 45 ++++++- src/rules/valid-mock-module-path.ts | 121 ++++++++++++++++++ src/rules/valid-mocked-module-path.ts | 98 -------------- 8 files changed, 198 insertions(+), 104 deletions(-) rename docs/rules/{valid-mocked-module-path.md => valid-mock-module-path.md} (64%) create mode 100644 src/rules/__tests__/fixtures/module/jsx/foo.jsx create mode 100644 src/rules/__tests__/fixtures/module/tsx/foo.jsx rename src/rules/__tests__/{valid-mocked-module-path.test.ts => valid-mock-module-path.test.ts} (61%) create mode 100644 src/rules/valid-mock-module-path.ts delete mode 100644 src/rules/valid-mocked-module-path.ts diff --git a/README.md b/README.md index 765a5ff95..b5843b731 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ Manually fixable by | [valid-describe-callback](docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback | ✅ | | | | | [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | 🔧 | | | [valid-expect-in-promise](docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | | -| [valid-mocked-module-path](docs/rules/valid-mocked-module-path.md) | Disallow mocking of non-existing module path | | | | | +| [valid-mock-module-path](docs/rules/valid-mock-module-path.md) | Disallow mocking of non-existing module path | | | | | | [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ✅ | | 🔧 | | ### Requires Type Checking diff --git a/docs/rules/valid-mocked-module-path.md b/docs/rules/valid-mock-module-path.md similarity index 64% rename from docs/rules/valid-mocked-module-path.md rename to docs/rules/valid-mock-module-path.md index 8e19fdcb3..e7818ad81 100644 --- a/docs/rules/valid-mocked-module-path.md +++ b/docs/rules/valid-mock-module-path.md @@ -1,4 +1,4 @@ -# Disallow mocking of non-existing module path (`valid-mocked-module-path`) +# Disallow mocking of non-existing module path (`valid-mock-module-path`) @@ -38,6 +38,36 @@ jest.mock('../../this/module/really/does/exist'); jest.mock('../../this/path/really/does/exist.js'); ``` +## Options + +```json +{ + "jest/valid-mock-module-path": [ + "error", + { + "moduleFileExtensions": [".tsx", ".ts"] + } + ] +} +``` + +### `moduleFileExtensions` + +This array option controls which file extensions the plugin checks for +existence. Valid values are: + +- `".js"` +- `".ts"` +- `".jsx"` +- `".tsx"` +- `".json"` + +For any custom extension, a preceding dot **must** be present before the file +extension for desired effect. + +The default value for this option is +`{ "moduleFileExtensions": [".js", ".ts", ".jsx", ".tsx", ".json"] }`. + ## When Not To Use It Don't use this rule on non-jest test files. diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index 3333b477d..6bf462676 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -72,7 +72,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/valid-describe-callback": "error", "jest/valid-expect": "error", "jest/valid-expect-in-promise": "error", - "jest/valid-mocked-module-path": "error", + "jest/valid-mock-module-path": "error", "jest/valid-title": "error", }, }, @@ -165,7 +165,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/valid-describe-callback": "error", "jest/valid-expect": "error", "jest/valid-expect-in-promise": "error", - "jest/valid-mocked-module-path": "error", + "jest/valid-mock-module-path": "error", "jest/valid-title": "error", }, }, diff --git a/src/rules/__tests__/fixtures/module/jsx/foo.jsx b/src/rules/__tests__/fixtures/module/jsx/foo.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/rules/__tests__/fixtures/module/tsx/foo.jsx b/src/rules/__tests__/fixtures/module/tsx/foo.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/rules/__tests__/valid-mocked-module-path.test.ts b/src/rules/__tests__/valid-mock-module-path.test.ts similarity index 61% rename from src/rules/__tests__/valid-mocked-module-path.test.ts rename to src/rules/__tests__/valid-mock-module-path.test.ts index 509f3960a..ab772bc9e 100644 --- a/src/rules/__tests__/valid-mocked-module-path.test.ts +++ b/src/rules/__tests__/valid-mock-module-path.test.ts @@ -1,5 +1,5 @@ import dedent from 'dedent'; -import rule from '../valid-mocked-module-path'; +import rule from '../valid-mock-module-path'; import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils'; const ruleTester = new RuleTester({ @@ -9,7 +9,7 @@ const ruleTester = new RuleTester({ }, }); -ruleTester.run('valid-mocked-module-path', rule, { +ruleTester.run('valid-mock-module-path', rule, { valid: [ { filename: __filename, code: 'jest.mock("./fixtures/module")' }, { filename: __filename, code: 'jest.mock("./fixtures/module", () => {})' }, @@ -32,6 +32,21 @@ ruleTester.run('valid-mocked-module-path', rule, { 'jest.mock("eslint")', 'jest.doMock("eslint")', 'jest.mock("child_process")', + 'jest.mock(() => {})', + { + filename: __filename, + code: dedent` + const a = "../module/does/not/exist"; + jest.mock(a); + `, + }, + { filename: __filename, code: 'jest.mock("./fixtures/module/jsx/foo")' }, + { filename: __filename, code: 'jest.mock("./fixtures/module/tsx/foo")' }, + { + filename: __filename, + code: 'jest.mock("./fixtures/module/tsx/foo")', + options: [{ moduleFileExtensions: ['.jsx'] }], + }, ], invalid: [ { @@ -58,6 +73,32 @@ ruleTester.run('valid-mocked-module-path', rule, { }, ], }, + { + filename: __filename, + code: 'jest.mock("./fixtures/module/foo.jsx")', + options: [{ moduleFileExtensions: ['.tsx'] }], + errors: [ + { + messageId: 'invalidMockModulePath', + data: { moduleName: '"./fixtures/module/foo.jsx"' }, + column: 1, + line: 1, + }, + ], + }, + { + filename: __filename, + code: 'jest.mock("./fixtures/module/foo.jsx")', + options: [{ moduleFileExtensions: undefined }], + errors: [ + { + messageId: 'invalidMockModulePath', + data: { moduleName: '"./fixtures/module/foo.jsx"' }, + column: 1, + line: 1, + }, + ], + }, { filename: __filename, code: 'jest.mock("@doesnotexist/module")', diff --git a/src/rules/valid-mock-module-path.ts b/src/rules/valid-mock-module-path.ts new file mode 100644 index 000000000..ceb95d47a --- /dev/null +++ b/src/rules/valid-mock-module-path.ts @@ -0,0 +1,121 @@ +import { statSync } from 'fs'; +import path from 'path'; +import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; +import { + createRule, + findModuleName, + getAccessorValue, + isSupportedAccessor, + isTypeOfJestFnCall, +} from './utils'; + +export default createRule< + [ + Partial<{ + moduleFileExtensions: readonly string[]; + }>, + ], + 'invalidMockModulePath' +>({ + name: __filename, + meta: { + type: 'problem', + docs: { + description: 'Disallow mocking of non-existing module path', + }, + messages: { + invalidMockModulePath: 'Module path {{ moduleName }} does not exist', + }, + schema: [ + { + type: 'object', + properties: { + moduleFileExtensions: { + type: 'array', + items: { type: 'string' }, + additionalItems: false, + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + moduleFileExtensions: ['.js', '.ts', '.tsx', '.jsx', '.json'], + }, + ], + create( + context, + [{ moduleFileExtensions = ['.js', '.ts', '.tsx', '.jsx', '.json'] }], + ) { + return { + CallExpression(node: TSESTree.CallExpression): void { + if (node.callee.type !== AST_NODE_TYPES.MemberExpression) { + return; + } + + if ( + !node.arguments.length || + !isTypeOfJestFnCall(node, context, ['jest']) || + !( + isSupportedAccessor(node.callee.property) && + ['mock', 'doMock'].includes(getAccessorValue(node.callee.property)) + ) + ) { + return; + } + + const moduleName = findModuleName(node.arguments[0]); + + if (!moduleName) { + return; + } + + try { + if (!moduleName.value.startsWith('.')) { + require.resolve(moduleName.value); + + return; + } + + const resolvedModulePath = path.resolve( + path.dirname(context.filename), + moduleName.value, + ); + + const hasPossiblyModulePaths = ['', ...moduleFileExtensions].some( + ext => { + try { + statSync(`${resolvedModulePath}${ext}`); + + return true; + } catch { + return false; + } + }, + ); + + if (!hasPossiblyModulePaths) { + throw { code: 'MODULE_NOT_FOUND' }; + } + } catch (err: any) { + // Reports unexpected issues when attempt to verify mocked module path. + // The list of possible errors is non-exhaustive. + /* istanbul ignore if */ + if (!['MODULE_NOT_FOUND', 'ENOENT'].includes(err.code)) { + throw new Error( + `Error when trying to validate mock module path from \`jest.mock\`: ${err}`, + ); + } + + context.report({ + messageId: 'invalidMockModulePath', + data: { moduleName: moduleName.raw }, + node, + }); + } + }, + }; + }, +}); diff --git a/src/rules/valid-mocked-module-path.ts b/src/rules/valid-mocked-module-path.ts deleted file mode 100644 index c1cde56e4..000000000 --- a/src/rules/valid-mocked-module-path.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { statSync } from 'fs'; -import path from 'path'; -import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; -import { - createRule, - findModuleName, - getAccessorValue, - isSupportedAccessor, - isTypeOfJestFnCall, -} from './utils'; - -export default createRule({ - name: __filename, - meta: { - type: 'problem', - docs: { - description: 'Disallow mocking of non-existing module path', - }, - messages: { - invalidMockModulePath: 'Module path {{ moduleName }} does not exist', - }, - schema: [], - }, - defaultOptions: [], - create(context) { - return { - CallExpression(node: TSESTree.CallExpression): void { - const { callee } = node; - - if (callee.type !== AST_NODE_TYPES.MemberExpression) { - return; - } - - const { property } = callee; - - if ( - !node.arguments.length || - !isTypeOfJestFnCall(node, context, ['jest']) || - !( - isSupportedAccessor(property) && - ['mock', 'doMock'].includes(getAccessorValue(property)) - ) - ) { - return; - } - - const moduleName = findModuleName(node.arguments[0]); - - /* istanbul ignore if */ - if (!moduleName) { - throw new Error( - 'Cannot parse mocked module name from `jest.mock` - - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`', - ); - } - - try { - if (moduleName.value.startsWith('.')) { - const resolvedModulePath = path.resolve( - path.dirname(context.filename), - moduleName.value, - ); - - const hasPossiblyModulePaths = ['', '.js', '.ts'] - .map(ext => `${resolvedModulePath}${ext}`) - .some(modPath => { - try { - statSync(modPath); - - return true; - } catch { - return false; - } - }); - - if (!hasPossiblyModulePaths) { - throw { code: 'MODULE_NOT_FOUND' }; - } - } else { - require.resolve(moduleName.value); - } - } catch (err: any) { - // Skip over any unexpected issues when attempt to verify mocked module path. - // The list of possible errors is non-exhaustive. - /* istanbul ignore if */ - if (!['MODULE_NOT_FOUND', 'ENOENT'].includes(err.code)) { - return; - } - - context.report({ - messageId: 'invalidMockModulePath', - data: { moduleName: moduleName.raw }, - node, - }); - } - }, - }; - }, -}); From 4fab92e504fb5d23c8d0be6eb2d2ba2d0a1e673f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=97=20Tr=E1=BB=8Dng=20H=E1=BA=A3i?= <41283691+hainenber@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:04:13 +0700 Subject: [PATCH 10/14] Apply suggestions from @G-Rath's code review Co-authored-by: Gareth Jones <3151613+G-Rath@users.noreply.github.com> --- docs/rules/valid-mock-module-path.md | 4 ++-- src/rules/valid-mock-module-path.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rules/valid-mock-module-path.md b/docs/rules/valid-mock-module-path.md index e7818ad81..359228fbc 100644 --- a/docs/rules/valid-mock-module-path.md +++ b/docs/rules/valid-mock-module-path.md @@ -45,7 +45,7 @@ jest.mock('../../this/path/really/does/exist.js'); "jest/valid-mock-module-path": [ "error", { - "moduleFileExtensions": [".tsx", ".ts"] + "moduleFileExtensions": [".js", ".ts", ".jsx", ".tsx", ".json"] } ] } @@ -54,7 +54,7 @@ jest.mock('../../this/path/really/does/exist.js'); ### `moduleFileExtensions` This array option controls which file extensions the plugin checks for -existence. Valid values are: +existence. The default extensions are: - `".js"` - `".ts"` diff --git a/src/rules/valid-mock-module-path.ts b/src/rules/valid-mock-module-path.ts index ceb95d47a..342c1b6d5 100644 --- a/src/rules/valid-mock-module-path.ts +++ b/src/rules/valid-mock-module-path.ts @@ -21,7 +21,7 @@ export default createRule< meta: { type: 'problem', docs: { - description: 'Disallow mocking of non-existing module path', + description: 'Disallow mocking of non-existing module paths', }, messages: { invalidMockModulePath: 'Module path {{ moduleName }} does not exist', From 09da117c0f62c57a5b3b7e1bb4c2f77a82571d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=97=20Tr=E1=BB=8Dng=20H=E1=BA=A3i?= <41283691+hainenber@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:00:42 +0700 Subject: [PATCH 11/14] Apply 2nd suggestion from @G-Rath Co-authored-by: Gareth Jones <3151613+G-Rath@users.noreply.github.com> --- docs/rules/valid-mock-module-path.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/rules/valid-mock-module-path.md b/docs/rules/valid-mock-module-path.md index 359228fbc..cb44115cb 100644 --- a/docs/rules/valid-mock-module-path.md +++ b/docs/rules/valid-mock-module-path.md @@ -64,10 +64,6 @@ existence. The default extensions are: For any custom extension, a preceding dot **must** be present before the file extension for desired effect. - -The default value for this option is -`{ "moduleFileExtensions": [".js", ".ts", ".jsx", ".tsx", ".json"] }`. - ## When Not To Use It Don't use this rule on non-jest test files. From e92eb10b2399aa1ab385f26d20968125c7c2014c Mon Sep 17 00:00:00 2001 From: hainenber Date: Wed, 12 Nov 2025 21:38:48 +0700 Subject: [PATCH 12/14] feat: recast err to `unknown` + refactor to return happy path asap Signed-off-by: hainenber --- src/rules/__tests__/fixtures/module/bar.css | 6 +++++ src/rules/__tests__/fixtures/module/bar.json | 1 + .../__tests__/valid-mock-module-path.test.ts | 10 +++++++++ src/rules/valid-mock-module-path.ts | 22 ++++++++++--------- 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 src/rules/__tests__/fixtures/module/bar.css create mode 100644 src/rules/__tests__/fixtures/module/bar.json diff --git a/src/rules/__tests__/fixtures/module/bar.css b/src/rules/__tests__/fixtures/module/bar.css new file mode 100644 index 000000000..c5d5cfc0d --- /dev/null +++ b/src/rules/__tests__/fixtures/module/bar.css @@ -0,0 +1,6 @@ +.bar-container { + background-color: #f5f5f5; + padding: 16px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} diff --git a/src/rules/__tests__/fixtures/module/bar.json b/src/rules/__tests__/fixtures/module/bar.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/rules/__tests__/fixtures/module/bar.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/rules/__tests__/valid-mock-module-path.test.ts b/src/rules/__tests__/valid-mock-module-path.test.ts index ab772bc9e..87b89e685 100644 --- a/src/rules/__tests__/valid-mock-module-path.test.ts +++ b/src/rules/__tests__/valid-mock-module-path.test.ts @@ -47,6 +47,16 @@ ruleTester.run('valid-mock-module-path', rule, { code: 'jest.mock("./fixtures/module/tsx/foo")', options: [{ moduleFileExtensions: ['.jsx'] }], }, + { + filename: __filename, + code: 'jest.mock("./fixtures/module/bar")', + options: [{ moduleFileExtensions: ['.json'] }], + }, + { + filename: __filename, + code: 'jest.mock("./fixtures/module/bar")', + options: [{ moduleFileExtensions: ['.css'] }], + }, ], invalid: [ { diff --git a/src/rules/valid-mock-module-path.ts b/src/rules/valid-mock-module-path.ts index 342c1b6d5..0a380bb4e 100644 --- a/src/rules/valid-mock-module-path.ts +++ b/src/rules/valid-mock-module-path.ts @@ -96,25 +96,27 @@ export default createRule< }, ); - if (!hasPossiblyModulePaths) { - throw { code: 'MODULE_NOT_FOUND' }; + if (hasPossiblyModulePaths) { + return; } - } catch (err: any) { + } catch (err: unknown) { + const castedErr = err as NodeJS.ErrnoException; + // Reports unexpected issues when attempt to verify mocked module path. // The list of possible errors is non-exhaustive. /* istanbul ignore if */ - if (!['MODULE_NOT_FOUND', 'ENOENT'].includes(err.code)) { + if (castedErr.code !== 'MODULE_NOT_FOUND') { throw new Error( `Error when trying to validate mock module path from \`jest.mock\`: ${err}`, ); } - - context.report({ - messageId: 'invalidMockModulePath', - data: { moduleName: moduleName.raw }, - node, - }); } + + context.report({ + messageId: 'invalidMockModulePath', + data: { moduleName: moduleName.raw }, + node, + }); }, }; }, From d60997c9a9196a2c0d1852c28aeae4af96dac986 Mon Sep 17 00:00:00 2001 From: hainenber Date: Wed, 12 Nov 2025 21:42:24 +0700 Subject: [PATCH 13/14] chore: rerun `yarn regenerate-docs` Signed-off-by: hainenber --- README.md | 2 +- docs/rules/valid-mock-module-path.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b5843b731..675e0e4d3 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ Manually fixable by | [valid-describe-callback](docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback | ✅ | | | | | [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | 🔧 | | | [valid-expect-in-promise](docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | | -| [valid-mock-module-path](docs/rules/valid-mock-module-path.md) | Disallow mocking of non-existing module path | | | | | +| [valid-mock-module-path](docs/rules/valid-mock-module-path.md) | Disallow mocking of non-existing module paths | | | | | | [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ✅ | | 🔧 | | ### Requires Type Checking diff --git a/docs/rules/valid-mock-module-path.md b/docs/rules/valid-mock-module-path.md index cb44115cb..6dd13d45b 100644 --- a/docs/rules/valid-mock-module-path.md +++ b/docs/rules/valid-mock-module-path.md @@ -1,4 +1,4 @@ -# Disallow mocking of non-existing module path (`valid-mock-module-path`) +# Disallow mocking of non-existing module paths (`valid-mock-module-path`) @@ -64,6 +64,7 @@ existence. The default extensions are: For any custom extension, a preceding dot **must** be present before the file extension for desired effect. + ## When Not To Use It Don't use this rule on non-jest test files. From 36d37a3ea0434361cc6ab2cf37511c9a69c465f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=97=20Tr=E1=BB=8Dng=20H=E1=BA=A3i?= <41283691+hainenber@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:36:17 +0700 Subject: [PATCH 14/14] Update src/rules/valid-mock-module-path.ts Co-authored-by: Gareth Jones <3151613+G-Rath@users.noreply.github.com> --- src/rules/valid-mock-module-path.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/valid-mock-module-path.ts b/src/rules/valid-mock-module-path.ts index 0a380bb4e..77feabc08 100644 --- a/src/rules/valid-mock-module-path.ts +++ b/src/rules/valid-mock-module-path.ts @@ -100,7 +100,7 @@ export default createRule< return; } } catch (err: unknown) { - const castedErr = err as NodeJS.ErrnoException; + const castedErr = err as { code: string }; // Reports unexpected issues when attempt to verify mocked module path. // The list of possible errors is non-exhaustive.