diff --git a/README.md b/README.md index d2a2da7b..d3b91995 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,7 @@ export default defineConfig({ | [prefer-vi-mocked](docs/rules/prefer-vi-mocked.md) | require `vi.mocked()` over `fn as Mock` | | 🌐 | | 🔧 | | 💭 | | | [require-awaited-expect-poll](docs/rules/require-awaited-expect-poll.md) | ensure that every `expect.poll` call is awaited | | 🌐 | | | | | | | [require-hook](docs/rules/require-hook.md) | require setup and teardown to be within a hook | | 🌐 | | | | | | +| [require-import-vi-mock](docs/rules/require-import-vi-mock.md) | require usage of import in vi.mock() | | 🌐 | | 🔧 | | | | | [require-local-test-context-for-concurrent-snapshots](docs/rules/require-local-test-context-for-concurrent-snapshots.md) | require local Test Context for concurrent snapshot tests | ✅ | 🌐 | | | | | | | [require-mock-type-parameters](docs/rules/require-mock-type-parameters.md) | enforce using type parameters with vitest mock functions | | 🌐 | | 🔧 | | | | | [require-to-throw-message](docs/rules/require-to-throw-message.md) | require toThrow() to be called with an error message | | 🌐 | | | | | | diff --git a/docs/rules/require-import-vi-mock.md b/docs/rules/require-import-vi-mock.md new file mode 100644 index 00000000..d31c9f07 --- /dev/null +++ b/docs/rules/require-import-vi-mock.md @@ -0,0 +1,31 @@ +# Require usage of import in vi.mock() (`vitest/require-import-vi-mock`) + +⚠️ This rule _warns_ in the 🌐 `all` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +## Rule Details + +This rule enforce usage of `import` inside `vi.mock` usage + +Examples of **incorrect** code for this rule: + +```js +vi.mock('./foo.js') +``` + +Examples of **correct** code for this rule: + +```js +vi.mock(import('./foo.js')) +vi.mock(import('./foo.js'), { spy: true }) +vi.mock(import('./foo.js'), () => ({ + Foo: vi.fn(), +})) +``` diff --git a/src/index.ts b/src/index.ts index a1d3d035..54e8cd13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,6 +97,7 @@ const allRules = { 'valid-expect': 'warn', 'valid-title': 'warn', 'require-awaited-expect-poll': 'warn', + 'require-import-vi-mock': 'warn', } as const satisfies RuleList const recommendedRules = { diff --git a/src/rules/index.ts b/src/rules/index.ts index 30cdcd68..a029ceeb 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -66,6 +66,7 @@ import preferTodo from './prefer-todo' import preferViMocked from './prefer-vi-mocked' import requireAwaitedExpectPoll from './require-awaited-expect-poll' import requireHook from './require-hook' +import requireImportViMock from './require-import-vi-mock' import requireLocalTestContextForConcurrentSnapshots from './require-local-test-context-for-concurrent-snapshots' import requireMockTypeParameters from './require-mock-type-parameters' import requireToThrowMessage from './require-to-throw-message' @@ -144,6 +145,7 @@ export const rules = { 'prefer-vi-mocked': preferViMocked, 'require-awaited-expect-poll': requireAwaitedExpectPoll, 'require-hook': requireHook, + 'require-import-vi-mock': requireImportViMock, 'require-local-test-context-for-concurrent-snapshots': requireLocalTestContextForConcurrentSnapshots, 'require-mock-type-parameters': requireMockTypeParameters, diff --git a/src/rules/require-import-vi-mock.ts b/src/rules/require-import-vi-mock.ts new file mode 100644 index 00000000..9e86243f --- /dev/null +++ b/src/rules/require-import-vi-mock.ts @@ -0,0 +1,59 @@ +import { createEslintRule } from '../utils' +import { AST_NODE_TYPES } from '@typescript-eslint/utils' +import { parseVitestFnCall } from '../utils/parse-vitest-fn-call' + +export const RULE_NAME = 'require-import-vi-mock' + +export default createEslintRule({ + name: RULE_NAME, + meta: { + fixable: 'code', + type: 'suggestion', + docs: { + description: 'require usage of import in vi.mock()', + requiresTypeChecking: false, + recommended: false, + }, + messages: { + requireImport: "Replace '{{path}}' with import('{{path}}')", + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + // Only consider vi.mock() calls + if (node.callee.type !== AST_NODE_TYPES.MemberExpression) return + + const vitestCallFn = parseVitestFnCall(node, context) + + if (vitestCallFn?.type !== 'vi') { + return false + } + + const { property } = node.callee + if ( + property.type !== AST_NODE_TYPES.Identifier || + property.name !== 'mock' + ) { + return + } + + const pathArg = node.arguments[0] + if (pathArg && pathArg.type === AST_NODE_TYPES.Literal) { + context.report({ + messageId: 'requireImport', + data: { + path: pathArg.value, + }, + node: pathArg, + fix(fixer) { + return fixer.replaceText(pathArg, `import('${pathArg.value}')`) + }, + }) + } + }, + } + }, +}) diff --git a/tests/require-import-vi-mock.test.ts b/tests/require-import-vi-mock.test.ts new file mode 100644 index 00000000..8f7cc82f --- /dev/null +++ b/tests/require-import-vi-mock.test.ts @@ -0,0 +1,41 @@ +import rule, { RULE_NAME } from '../src/rules/require-import-vi-mock' +import { ruleTester } from './ruleTester' + +ruleTester.run(RULE_NAME, rule, { + valid: [ + 'vi.mock(import("./foo.js"))', + 'vi.mock(import("node:fs/promises"))', + 'vi.mock(import("./foo.js"), () => ({ Foo: vi.fn() }))', + 'vi.mock(import("./foo.js"), { spy: true });', + ], + invalid: [ + { + code: 'vi.mock("./foo.js")', + errors: [{ messageId: 'requireImport', column: 9, line: 1 }], + output: "vi.mock(import('./foo.js'))", + }, + { + code: 'vi.mock("node:fs/promises")', + errors: [{ messageId: 'requireImport', column: 9, line: 1 }], + output: "vi.mock(import('node:fs/promises'))", + }, + { + code: 'vi.mock("./foo.js", () => ({ Foo: vi.fn() }))', + errors: [{ messageId: 'requireImport', column: 9, line: 1 }], + output: "vi.mock(import('./foo.js'), () => ({ Foo: vi.fn() }))", + }, + { + code: ` + import { vi as renamedVi } from 'vitest'; + + renamedVi.mock('./foo.js', () => ({ Foo: vi.fn() })) + `, + errors: [{ messageId: 'requireImport', column: 24, line: 4 }], + output: ` + import { vi as renamedVi } from 'vitest'; + + renamedVi.mock(import('./foo.js'), () => ({ Foo: vi.fn() })) + `, + }, + ], +})