@vitest/eslint-plugin 1.5.4 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -251,12 +251,12 @@ export default defineConfig({
251
251
  | [prefer-to-be-object](docs/rules/prefer-to-be-object.md) | enforce using toBeObject() | | 🌐 | | 🔧 | | | |
252
252
  | [prefer-to-be-truthy](docs/rules/prefer-to-be-truthy.md) | enforce using `toBeTruthy` | | | 🌐 | 🔧 | | | |
253
253
  | [prefer-to-contain](docs/rules/prefer-to-contain.md) | enforce using toContain() | | 🌐 | | 🔧 | | | |
254
+ | [prefer-to-have-been-called-times](docs/rules/prefer-to-have-been-called-times.md) | Suggest using `toHaveBeenCalledTimes()` | | 🌐 | | 🔧 | | | |
254
255
  | [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | enforce using toHaveLength() | | 🌐 | | 🔧 | | | |
255
256
  | [prefer-todo](docs/rules/prefer-todo.md) | enforce using `test.todo` | | 🌐 | | 🔧 | | | |
256
257
  | [prefer-vi-mocked](docs/rules/prefer-vi-mocked.md) | require `vi.mocked()` over `fn as Mock` | | 🌐 | | 🔧 | | 💭 | |
257
258
  | [require-awaited-expect-poll](docs/rules/require-awaited-expect-poll.md) | ensure that every `expect.poll` call is awaited | | 🌐 | | | | | |
258
259
  | [require-hook](docs/rules/require-hook.md) | require setup and teardown to be within a hook | | 🌐 | | | | | |
259
- | [require-import-vi-mock](docs/rules/require-import-vi-mock.md) | require usage of import in vi.mock() | | 🌐 | | 🔧 | | | |
260
260
  | [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 | ✅ | 🌐 | | | | | |
261
261
  | [require-mock-type-parameters](docs/rules/require-mock-type-parameters.md) | enforce using type parameters with vitest mock functions | | 🌐 | | 🔧 | | | |
262
262
  | [require-to-throw-message](docs/rules/require-to-throw-message.md) | require toThrow() to be called with an error message | | 🌐 | | | | | |
package/dist/index.cjs CHANGED
@@ -31,7 +31,7 @@ let __typescript_eslint_scope_manager = require("@typescript-eslint/scope-manage
31
31
  __typescript_eslint_scope_manager = __toESM(__typescript_eslint_scope_manager);
32
32
 
33
33
  //#region package.json
34
- var version = "1.5.4";
34
+ var version = "1.6.3";
35
35
 
36
36
  //#endregion
37
37
  //#region src/utils/index.ts
@@ -1738,7 +1738,16 @@ const resolveScope = (scope, identifier) => {
1738
1738
  if ((property?.key.type === __typescript_eslint_utils.AST_NODE_TYPES.Identifier ? property.key : void 0)?.name === identifier) return "testContext";
1739
1739
  }
1740
1740
  /** if detect test function is created with `.extend()` */
1741
- if (def.node.type === __typescript_eslint_utils.AST_NODE_TYPES.VariableDeclarator && def.node.id.type === __typescript_eslint_utils.AST_NODE_TYPES.Identifier && Object.prototype.hasOwnProperty.call(TestCaseName, def.node.id.name) && def.node.init?.type === __typescript_eslint_utils.AST_NODE_TYPES.CallExpression && def.node.init.callee.type === __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && isIdentifier(def.node.init.callee.property, "extend")) return "testContext";
1741
+ if (def.node.type === __typescript_eslint_utils.AST_NODE_TYPES.VariableDeclarator && def.node.id.type === __typescript_eslint_utils.AST_NODE_TYPES.Identifier && def.node.init?.type === __typescript_eslint_utils.AST_NODE_TYPES.CallExpression && def.node.init.callee.type === __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && isIdentifier(def.node.init.callee.property, "extend")) {
1742
+ const rootName = getNodeName(def.node.init.callee.object)?.split(".")[0];
1743
+ if (rootName && rootName !== identifier) {
1744
+ const resolved = resolveScope(currentScope, rootName);
1745
+ if (resolved && typeof resolved === "object" && Object.hasOwn(TestCaseName, resolved.imported)) return {
1746
+ ...resolved,
1747
+ local: identifier
1748
+ };
1749
+ }
1750
+ }
1742
1751
  const namedParam = isFunction(def.node) ? def.node.params.find((params) => params.type === __typescript_eslint_utils.AST_NODE_TYPES.Identifier) : void 0;
1743
1752
  if (namedParam && isAncestorTestCaseCall(namedParam.parent)) return "testContext";
1744
1753
  const importDetails = describePossibleImportDef(def);
@@ -4719,21 +4728,30 @@ var prefer_import_in_mock_default = createEslintRule({
4719
4728
  type: "suggestion",
4720
4729
  docs: { description: "prefer dynamic import in mock" },
4721
4730
  messages: { preferImport: "Replace '{{path}}' with import('{{path}}')" },
4722
- schema: []
4731
+ schema: [{
4732
+ type: "object",
4733
+ properties: { fixable: {
4734
+ type: "boolean",
4735
+ default: true
4736
+ } },
4737
+ additionalProperties: false
4738
+ }]
4723
4739
  },
4724
- defaultOptions: [],
4725
- create(context) {
4740
+ defaultOptions: [{ fixable: true }],
4741
+ create(context, options) {
4742
+ const fixable = options[0].fixable;
4726
4743
  return { CallExpression(node) {
4727
4744
  if (node.callee.type !== __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression) return;
4728
- const { object, property } = node.callee;
4729
- if (object.type !== __typescript_eslint_utils.AST_NODE_TYPES.Identifier || object.name !== "vi" || property.type !== __typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
4730
- const apiName = property.name;
4745
+ if (parseVitestFnCall(node, context)?.type !== "vi") return false;
4746
+ const { property } = node.callee;
4747
+ if (property.type != __typescript_eslint_utils.AST_NODE_TYPES.Identifier || property.name != "mock") return;
4731
4748
  const pathArg = node.arguments[0];
4732
- if (apiName === "mock" && pathArg && pathArg.type === __typescript_eslint_utils.AST_NODE_TYPES.Literal) context.report({
4749
+ if (pathArg && pathArg.type === __typescript_eslint_utils.AST_NODE_TYPES.Literal) context.report({
4733
4750
  messageId: "preferImport",
4734
4751
  data: { path: pathArg.value },
4735
4752
  node,
4736
4753
  fix(fixer) {
4754
+ if (!fixable) return null;
4737
4755
  return fixer.replaceText(pathArg, `import('${pathArg.value}')`);
4738
4756
  }
4739
4757
  });
@@ -5657,47 +5675,11 @@ var require_hook_default = createEslintRule({
5657
5675
  }
5658
5676
  });
5659
5677
 
5660
- //#endregion
5661
- //#region src/rules/require-import-vi-mock.ts
5662
- const RULE_NAME$10 = "require-import-vi-mock";
5663
- var require_import_vi_mock_default = createEslintRule({
5664
- name: RULE_NAME$10,
5665
- meta: {
5666
- fixable: "code",
5667
- type: "suggestion",
5668
- docs: {
5669
- description: "require usage of import in vi.mock()",
5670
- requiresTypeChecking: false,
5671
- recommended: false
5672
- },
5673
- messages: { requireImport: "Replace '{{path}}' with import('{{path}}')" },
5674
- schema: []
5675
- },
5676
- defaultOptions: [],
5677
- create(context) {
5678
- return { CallExpression(node) {
5679
- if (node.callee.type !== __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression) return;
5680
- if (parseVitestFnCall(node, context)?.type !== "vi") return false;
5681
- const { property } = node.callee;
5682
- if (property.type !== __typescript_eslint_utils.AST_NODE_TYPES.Identifier || property.name !== "mock") return;
5683
- const pathArg = node.arguments[0];
5684
- if (pathArg && pathArg.type === __typescript_eslint_utils.AST_NODE_TYPES.Literal) context.report({
5685
- messageId: "requireImport",
5686
- data: { path: pathArg.value },
5687
- node: pathArg,
5688
- fix(fixer) {
5689
- return fixer.replaceText(pathArg, `import('${pathArg.value}')`);
5690
- }
5691
- });
5692
- } };
5693
- }
5694
- });
5695
-
5696
5678
  //#endregion
5697
5679
  //#region src/rules/require-local-test-context-for-concurrent-snapshots.ts
5698
- const RULE_NAME$9 = "require-local-test-context-for-concurrent-snapshots";
5680
+ const RULE_NAME$10 = "require-local-test-context-for-concurrent-snapshots";
5699
5681
  var require_local_test_context_for_concurrent_snapshots_default = createEslintRule({
5700
- name: RULE_NAME$9,
5682
+ name: RULE_NAME$10,
5701
5683
  meta: {
5702
5684
  docs: {
5703
5685
  description: "require local Test Context for concurrent snapshot tests",
@@ -5738,9 +5720,9 @@ var require_local_test_context_for_concurrent_snapshots_default = createEslintRu
5738
5720
 
5739
5721
  //#endregion
5740
5722
  //#region src/rules/require-mock-type-parameters.ts
5741
- const RULE_NAME$8 = "require-mock-type-parameters";
5723
+ const RULE_NAME$9 = "require-mock-type-parameters";
5742
5724
  var require_mock_type_parameters_default = createEslintRule({
5743
- name: RULE_NAME$8,
5725
+ name: RULE_NAME$9,
5744
5726
  meta: {
5745
5727
  type: "suggestion",
5746
5728
  docs: {
@@ -5777,9 +5759,9 @@ var require_mock_type_parameters_default = createEslintRule({
5777
5759
 
5778
5760
  //#endregion
5779
5761
  //#region src/rules/require-to-throw-message.ts
5780
- const RULE_NAME$7 = "require-to-throw-message";
5762
+ const RULE_NAME$8 = "require-to-throw-message";
5781
5763
  var require_to_throw_message_default = createEslintRule({
5782
- name: RULE_NAME$7,
5764
+ name: RULE_NAME$8,
5783
5765
  meta: {
5784
5766
  type: "suggestion",
5785
5767
  docs: {
@@ -5807,9 +5789,9 @@ var require_to_throw_message_default = createEslintRule({
5807
5789
 
5808
5790
  //#endregion
5809
5791
  //#region src/rules/require-top-level-describe.ts
5810
- const RULE_NAME$6 = "require-top-level-describe";
5792
+ const RULE_NAME$7 = "require-top-level-describe";
5811
5793
  var require_top_level_describe_default = createEslintRule({
5812
- name: RULE_NAME$6,
5794
+ name: RULE_NAME$7,
5813
5795
  meta: {
5814
5796
  docs: {
5815
5797
  description: "enforce that all tests are in a top-level describe",
@@ -5878,7 +5860,7 @@ var require_top_level_describe_default = createEslintRule({
5878
5860
 
5879
5861
  //#endregion
5880
5862
  //#region src/rules/valid-describe-callback.ts
5881
- const RULE_NAME$5 = "valid-describe-callback";
5863
+ const RULE_NAME$6 = "valid-describe-callback";
5882
5864
  const paramsLocation = (params) => {
5883
5865
  const [first] = params;
5884
5866
  const last = params[params.length - 1];
@@ -5900,7 +5882,7 @@ const reportUnexpectedReturnInDescribe = (blockStatement, context) => {
5900
5882
  });
5901
5883
  };
5902
5884
  var valid_describe_callback_default = createEslintRule({
5903
- name: RULE_NAME$5,
5885
+ name: RULE_NAME$6,
5904
5886
  meta: {
5905
5887
  type: "problem",
5906
5888
  docs: {
@@ -5967,7 +5949,7 @@ var valid_describe_callback_default = createEslintRule({
5967
5949
 
5968
5950
  //#endregion
5969
5951
  //#region src/rules/valid-expect-in-promise.ts
5970
- const RULE_NAME$4 = "valid-expect-in-promise";
5952
+ const RULE_NAME$5 = "valid-expect-in-promise";
5971
5953
  const defaultAsyncMatchers$1 = ["toRejectWith", "toResolveWith"];
5972
5954
  const isPromiseChainCall = (node) => {
5973
5955
  if (node.type === __typescript_eslint_utils.AST_NODE_TYPES.CallExpression && node.callee.type === __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.callee.property)) {
@@ -6099,7 +6081,7 @@ const isVariableAwaitedOrReturned = (variable, context) => {
6099
6081
  return isValueAwaitedOrReturned(variable.id, body, context);
6100
6082
  };
6101
6083
  var valid_expect_in_promise_default = createEslintRule({
6102
- name: RULE_NAME$4,
6084
+ name: RULE_NAME$5,
6103
6085
  meta: {
6104
6086
  docs: { description: "require promises that have expectations in their chain to be valid" },
6105
6087
  messages: { expectInFloatingPromise: "This promise should either be returned or awaited to ensure the expects in its chain are called" },
@@ -6159,7 +6141,7 @@ var valid_expect_in_promise_default = createEslintRule({
6159
6141
 
6160
6142
  //#endregion
6161
6143
  //#region src/rules/valid-expect.ts
6162
- const RULE_NAME$3 = "valid-expect";
6144
+ const RULE_NAME$4 = "valid-expect";
6163
6145
  const defaultAsyncMatchers = ["toReject", "toResolve"];
6164
6146
  /**
6165
6147
  * Async assertions might be called in Promise
@@ -6194,7 +6176,7 @@ const isAcceptableReturnNode = (node, allowReturn) => {
6194
6176
  return [__typescript_eslint_utils.AST_NODE_TYPES.ArrowFunctionExpression, __typescript_eslint_utils.AST_NODE_TYPES.AwaitExpression].includes(node.type);
6195
6177
  };
6196
6178
  var valid_expect_default = createEslintRule({
6197
- name: RULE_NAME$3,
6179
+ name: RULE_NAME$4,
6198
6180
  meta: {
6199
6181
  docs: {
6200
6182
  description: "enforce valid `expect()` usage",
@@ -6387,7 +6369,7 @@ var valid_expect_default = createEslintRule({
6387
6369
 
6388
6370
  //#endregion
6389
6371
  //#region src/rules/valid-title.ts
6390
- const RULE_NAME$2 = "valid-title";
6372
+ const RULE_NAME$3 = "valid-title";
6391
6373
  const trimFXPrefix = (word) => ["f", "x"].includes(word.charAt(0)) ? word.substring(1) : word;
6392
6374
  const quoteStringValue = (node) => node.type === __typescript_eslint_utils.AST_NODE_TYPES.TemplateLiteral ? `\`${node.quasis[0].value.raw}\`` : node.raw;
6393
6375
  const MatcherAndMessageSchema = {
@@ -6426,7 +6408,7 @@ const doesBinaryExpressionContainStringNode = (binaryExp) => {
6426
6408
  return isStringNode(binaryExp.left);
6427
6409
  };
6428
6410
  var valid_title_default = createEslintRule({
6429
- name: RULE_NAME$2,
6411
+ name: RULE_NAME$3,
6430
6412
  meta: {
6431
6413
  docs: {
6432
6414
  description: "enforce valid titles",
@@ -6579,9 +6561,9 @@ var valid_title_default = createEslintRule({
6579
6561
 
6580
6562
  //#endregion
6581
6563
  //#region src/rules/warn-todo.ts
6582
- const RULE_NAME$1 = "warn-todo";
6564
+ const RULE_NAME$2 = "warn-todo";
6583
6565
  var warn_todo_default = createEslintRule({
6584
- name: RULE_NAME$1,
6566
+ name: RULE_NAME$2,
6585
6567
  meta: {
6586
6568
  docs: {
6587
6569
  description: "disallow `.todo` usage",
@@ -6608,7 +6590,7 @@ var warn_todo_default = createEslintRule({
6608
6590
 
6609
6591
  //#endregion
6610
6592
  //#region src/rules/no-unneeded-async-expect-function.ts
6611
- const RULE_NAME = "no-unneeded-async-expect-function";
6593
+ const RULE_NAME$1 = "no-unneeded-async-expect-function";
6612
6594
  const getAwaitedCallExpression = (expression) => {
6613
6595
  if (!expression.async) return null;
6614
6596
  if (expression.type === __typescript_eslint_utils.AST_NODE_TYPES.ArrowFunctionExpression && expression.body.type === __typescript_eslint_utils.AST_NODE_TYPES.AwaitExpression && expression.body.argument.type === __typescript_eslint_utils.AST_NODE_TYPES.CallExpression) return expression.body.argument;
@@ -6618,7 +6600,7 @@ const getAwaitedCallExpression = (expression) => {
6618
6600
  return null;
6619
6601
  };
6620
6602
  var no_unneeded_async_expect_function_default = createEslintRule({
6621
- name: RULE_NAME,
6603
+ name: RULE_NAME$1,
6622
6604
  meta: {
6623
6605
  docs: { description: "Disallow unnecessary async function wrapper for expected promises" },
6624
6606
  fixable: "code",
@@ -6649,6 +6631,42 @@ var no_unneeded_async_expect_function_default = createEslintRule({
6649
6631
  }
6650
6632
  });
6651
6633
 
6634
+ //#endregion
6635
+ //#region src/rules/prefer-to-have-been-called-times.ts
6636
+ const RULE_NAME = "prefer-to-have-been-called-times";
6637
+ var prefer_to_have_been_called_times_default = createEslintRule({
6638
+ name: RULE_NAME,
6639
+ meta: {
6640
+ fixable: "code",
6641
+ docs: { description: "Suggest using `toHaveBeenCalledTimes()`" },
6642
+ messages: { preferMatcher: "Prefer `toHaveBeenCalledTimes`" },
6643
+ type: "suggestion",
6644
+ schema: []
6645
+ },
6646
+ defaultOptions: [],
6647
+ create(context) {
6648
+ return { CallExpression(node) {
6649
+ const vitestFnCall = parseVitestFnCall(node, context);
6650
+ if (vitestFnCall?.type !== "expect") return;
6651
+ const { parent: expect } = vitestFnCall.head.node;
6652
+ if (expect?.type !== __typescript_eslint_utils.AST_NODE_TYPES.CallExpression) return;
6653
+ const { matcher } = vitestFnCall;
6654
+ if (!isSupportedAccessor(matcher, "toHaveLength")) return;
6655
+ const [argument] = expect.arguments;
6656
+ if (argument?.type !== __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || !isSupportedAccessor(argument.property, "calls")) return;
6657
+ const { object } = argument;
6658
+ if (object.type !== __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || !isSupportedAccessor(object.property, "mock")) return;
6659
+ context.report({
6660
+ messageId: "preferMatcher",
6661
+ node: matcher,
6662
+ fix(fixer) {
6663
+ return [fixer.removeRange([object.property.range[0] - 1, argument.range[1]]), fixer.replaceTextRange([matcher.parent.object.range[1], matcher.parent.range[1]], ".toHaveBeenCalledTimes")];
6664
+ }
6665
+ });
6666
+ } };
6667
+ }
6668
+ });
6669
+
6652
6670
  //#endregion
6653
6671
  //#region src/rules/index.ts
6654
6672
  const rules = {
@@ -6716,12 +6734,12 @@ const rules = {
6716
6734
  "prefer-to-be-truthy": prefer_to_be_truthy_default,
6717
6735
  "prefer-to-be": prefer_to_be_default,
6718
6736
  "prefer-to-contain": prefer_to_contain_default,
6737
+ "prefer-to-have-been-called-times": prefer_to_have_been_called_times_default,
6719
6738
  "prefer-to-have-length": prefer_to_have_length_default,
6720
6739
  "prefer-todo": prefer_todo_default,
6721
6740
  "prefer-vi-mocked": prefer_vi_mocked_default,
6722
6741
  "require-awaited-expect-poll": require_awaited_expect_poll_default,
6723
6742
  "require-hook": require_hook_default,
6724
- "require-import-vi-mock": require_import_vi_mock_default,
6725
6743
  "require-local-test-context-for-concurrent-snapshots": require_local_test_context_for_concurrent_snapshots_default,
6726
6744
  "require-mock-type-parameters": require_mock_type_parameters_default,
6727
6745
  "require-to-throw-message": require_to_throw_message_default,
@@ -6814,6 +6832,7 @@ const allRules = {
6814
6832
  "prefer-to-be-truthy": "off",
6815
6833
  "prefer-to-be": "warn",
6816
6834
  "prefer-to-contain": "warn",
6835
+ "prefer-to-have-been-called-times": "warn",
6817
6836
  "prefer-to-have-length": "warn",
6818
6837
  "prefer-todo": "warn",
6819
6838
  "prefer-vi-mocked": "warn",
@@ -6826,8 +6845,7 @@ const allRules = {
6826
6845
  "valid-expect-in-promise": "warn",
6827
6846
  "valid-expect": "warn",
6828
6847
  "valid-title": "warn",
6829
- "require-awaited-expect-poll": "warn",
6830
- "require-import-vi-mock": "warn"
6848
+ "require-awaited-expect-poll": "warn"
6831
6849
  };
6832
6850
  const recommendedRules = {
6833
6851
  "expect-expect": "error",
package/dist/index.d.cts CHANGED
@@ -133,6 +133,7 @@ declare const plugin: {
133
133
  readonly "vitest/prefer-to-be-truthy": "off";
134
134
  readonly "vitest/prefer-to-be": "warn";
135
135
  readonly "vitest/prefer-to-contain": "warn";
136
+ readonly "vitest/prefer-to-have-been-called-times": "warn";
136
137
  readonly "vitest/prefer-to-have-length": "warn";
137
138
  readonly "vitest/prefer-todo": "warn";
138
139
  readonly "vitest/prefer-vi-mocked": "warn";
@@ -146,7 +147,6 @@ declare const plugin: {
146
147
  readonly "vitest/valid-expect": "warn";
147
148
  readonly "vitest/valid-title": "warn";
148
149
  readonly "vitest/require-awaited-expect-poll": "warn";
149
- readonly "vitest/require-import-vi-mock": "warn";
150
150
  };
151
151
  };
152
152
  readonly env: {
package/dist/index.d.ts CHANGED
@@ -133,6 +133,7 @@ declare const plugin: {
133
133
  readonly "vitest/prefer-to-be-truthy": "off";
134
134
  readonly "vitest/prefer-to-be": "warn";
135
135
  readonly "vitest/prefer-to-contain": "warn";
136
+ readonly "vitest/prefer-to-have-been-called-times": "warn";
136
137
  readonly "vitest/prefer-to-have-length": "warn";
137
138
  readonly "vitest/prefer-todo": "warn";
138
139
  readonly "vitest/prefer-vi-mocked": "warn";
@@ -146,7 +147,6 @@ declare const plugin: {
146
147
  readonly "vitest/valid-expect": "warn";
147
148
  readonly "vitest/valid-title": "warn";
148
149
  readonly "vitest/require-awaited-expect-poll": "warn";
149
- readonly "vitest/require-import-vi-mock": "warn";
150
150
  };
151
151
  };
152
152
  readonly env: {
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { isAbsolute, posix } from "node:path";
4
4
  import { DefinitionType } from "@typescript-eslint/scope-manager";
5
5
 
6
6
  //#region package.json
7
- var version = "1.5.4";
7
+ var version = "1.6.3";
8
8
 
9
9
  //#endregion
10
10
  //#region src/utils/index.ts
@@ -1711,7 +1711,16 @@ const resolveScope = (scope, identifier) => {
1711
1711
  if ((property?.key.type === AST_NODE_TYPES.Identifier ? property.key : void 0)?.name === identifier) return "testContext";
1712
1712
  }
1713
1713
  /** if detect test function is created with `.extend()` */
1714
- if (def.node.type === AST_NODE_TYPES.VariableDeclarator && def.node.id.type === AST_NODE_TYPES.Identifier && Object.prototype.hasOwnProperty.call(TestCaseName, def.node.id.name) && def.node.init?.type === AST_NODE_TYPES.CallExpression && def.node.init.callee.type === AST_NODE_TYPES.MemberExpression && isIdentifier(def.node.init.callee.property, "extend")) return "testContext";
1714
+ if (def.node.type === AST_NODE_TYPES.VariableDeclarator && def.node.id.type === AST_NODE_TYPES.Identifier && def.node.init?.type === AST_NODE_TYPES.CallExpression && def.node.init.callee.type === AST_NODE_TYPES.MemberExpression && isIdentifier(def.node.init.callee.property, "extend")) {
1715
+ const rootName = getNodeName(def.node.init.callee.object)?.split(".")[0];
1716
+ if (rootName && rootName !== identifier) {
1717
+ const resolved = resolveScope(currentScope, rootName);
1718
+ if (resolved && typeof resolved === "object" && Object.hasOwn(TestCaseName, resolved.imported)) return {
1719
+ ...resolved,
1720
+ local: identifier
1721
+ };
1722
+ }
1723
+ }
1715
1724
  const namedParam = isFunction(def.node) ? def.node.params.find((params) => params.type === AST_NODE_TYPES.Identifier) : void 0;
1716
1725
  if (namedParam && isAncestorTestCaseCall(namedParam.parent)) return "testContext";
1717
1726
  const importDetails = describePossibleImportDef(def);
@@ -4692,21 +4701,30 @@ var prefer_import_in_mock_default = createEslintRule({
4692
4701
  type: "suggestion",
4693
4702
  docs: { description: "prefer dynamic import in mock" },
4694
4703
  messages: { preferImport: "Replace '{{path}}' with import('{{path}}')" },
4695
- schema: []
4704
+ schema: [{
4705
+ type: "object",
4706
+ properties: { fixable: {
4707
+ type: "boolean",
4708
+ default: true
4709
+ } },
4710
+ additionalProperties: false
4711
+ }]
4696
4712
  },
4697
- defaultOptions: [],
4698
- create(context) {
4713
+ defaultOptions: [{ fixable: true }],
4714
+ create(context, options) {
4715
+ const fixable = options[0].fixable;
4699
4716
  return { CallExpression(node) {
4700
4717
  if (node.callee.type !== AST_NODE_TYPES.MemberExpression) return;
4701
- const { object, property } = node.callee;
4702
- if (object.type !== AST_NODE_TYPES.Identifier || object.name !== "vi" || property.type !== AST_NODE_TYPES.Identifier) return;
4703
- const apiName = property.name;
4718
+ if (parseVitestFnCall(node, context)?.type !== "vi") return false;
4719
+ const { property } = node.callee;
4720
+ if (property.type != AST_NODE_TYPES.Identifier || property.name != "mock") return;
4704
4721
  const pathArg = node.arguments[0];
4705
- if (apiName === "mock" && pathArg && pathArg.type === AST_NODE_TYPES.Literal) context.report({
4722
+ if (pathArg && pathArg.type === AST_NODE_TYPES.Literal) context.report({
4706
4723
  messageId: "preferImport",
4707
4724
  data: { path: pathArg.value },
4708
4725
  node,
4709
4726
  fix(fixer) {
4727
+ if (!fixable) return null;
4710
4728
  return fixer.replaceText(pathArg, `import('${pathArg.value}')`);
4711
4729
  }
4712
4730
  });
@@ -5630,47 +5648,11 @@ var require_hook_default = createEslintRule({
5630
5648
  }
5631
5649
  });
5632
5650
 
5633
- //#endregion
5634
- //#region src/rules/require-import-vi-mock.ts
5635
- const RULE_NAME$10 = "require-import-vi-mock";
5636
- var require_import_vi_mock_default = createEslintRule({
5637
- name: RULE_NAME$10,
5638
- meta: {
5639
- fixable: "code",
5640
- type: "suggestion",
5641
- docs: {
5642
- description: "require usage of import in vi.mock()",
5643
- requiresTypeChecking: false,
5644
- recommended: false
5645
- },
5646
- messages: { requireImport: "Replace '{{path}}' with import('{{path}}')" },
5647
- schema: []
5648
- },
5649
- defaultOptions: [],
5650
- create(context) {
5651
- return { CallExpression(node) {
5652
- if (node.callee.type !== AST_NODE_TYPES.MemberExpression) return;
5653
- if (parseVitestFnCall(node, context)?.type !== "vi") return false;
5654
- const { property } = node.callee;
5655
- if (property.type !== AST_NODE_TYPES.Identifier || property.name !== "mock") return;
5656
- const pathArg = node.arguments[0];
5657
- if (pathArg && pathArg.type === AST_NODE_TYPES.Literal) context.report({
5658
- messageId: "requireImport",
5659
- data: { path: pathArg.value },
5660
- node: pathArg,
5661
- fix(fixer) {
5662
- return fixer.replaceText(pathArg, `import('${pathArg.value}')`);
5663
- }
5664
- });
5665
- } };
5666
- }
5667
- });
5668
-
5669
5651
  //#endregion
5670
5652
  //#region src/rules/require-local-test-context-for-concurrent-snapshots.ts
5671
- const RULE_NAME$9 = "require-local-test-context-for-concurrent-snapshots";
5653
+ const RULE_NAME$10 = "require-local-test-context-for-concurrent-snapshots";
5672
5654
  var require_local_test_context_for_concurrent_snapshots_default = createEslintRule({
5673
- name: RULE_NAME$9,
5655
+ name: RULE_NAME$10,
5674
5656
  meta: {
5675
5657
  docs: {
5676
5658
  description: "require local Test Context for concurrent snapshot tests",
@@ -5711,9 +5693,9 @@ var require_local_test_context_for_concurrent_snapshots_default = createEslintRu
5711
5693
 
5712
5694
  //#endregion
5713
5695
  //#region src/rules/require-mock-type-parameters.ts
5714
- const RULE_NAME$8 = "require-mock-type-parameters";
5696
+ const RULE_NAME$9 = "require-mock-type-parameters";
5715
5697
  var require_mock_type_parameters_default = createEslintRule({
5716
- name: RULE_NAME$8,
5698
+ name: RULE_NAME$9,
5717
5699
  meta: {
5718
5700
  type: "suggestion",
5719
5701
  docs: {
@@ -5750,9 +5732,9 @@ var require_mock_type_parameters_default = createEslintRule({
5750
5732
 
5751
5733
  //#endregion
5752
5734
  //#region src/rules/require-to-throw-message.ts
5753
- const RULE_NAME$7 = "require-to-throw-message";
5735
+ const RULE_NAME$8 = "require-to-throw-message";
5754
5736
  var require_to_throw_message_default = createEslintRule({
5755
- name: RULE_NAME$7,
5737
+ name: RULE_NAME$8,
5756
5738
  meta: {
5757
5739
  type: "suggestion",
5758
5740
  docs: {
@@ -5780,9 +5762,9 @@ var require_to_throw_message_default = createEslintRule({
5780
5762
 
5781
5763
  //#endregion
5782
5764
  //#region src/rules/require-top-level-describe.ts
5783
- const RULE_NAME$6 = "require-top-level-describe";
5765
+ const RULE_NAME$7 = "require-top-level-describe";
5784
5766
  var require_top_level_describe_default = createEslintRule({
5785
- name: RULE_NAME$6,
5767
+ name: RULE_NAME$7,
5786
5768
  meta: {
5787
5769
  docs: {
5788
5770
  description: "enforce that all tests are in a top-level describe",
@@ -5851,7 +5833,7 @@ var require_top_level_describe_default = createEslintRule({
5851
5833
 
5852
5834
  //#endregion
5853
5835
  //#region src/rules/valid-describe-callback.ts
5854
- const RULE_NAME$5 = "valid-describe-callback";
5836
+ const RULE_NAME$6 = "valid-describe-callback";
5855
5837
  const paramsLocation = (params) => {
5856
5838
  const [first] = params;
5857
5839
  const last = params[params.length - 1];
@@ -5873,7 +5855,7 @@ const reportUnexpectedReturnInDescribe = (blockStatement, context) => {
5873
5855
  });
5874
5856
  };
5875
5857
  var valid_describe_callback_default = createEslintRule({
5876
- name: RULE_NAME$5,
5858
+ name: RULE_NAME$6,
5877
5859
  meta: {
5878
5860
  type: "problem",
5879
5861
  docs: {
@@ -5940,7 +5922,7 @@ var valid_describe_callback_default = createEslintRule({
5940
5922
 
5941
5923
  //#endregion
5942
5924
  //#region src/rules/valid-expect-in-promise.ts
5943
- const RULE_NAME$4 = "valid-expect-in-promise";
5925
+ const RULE_NAME$5 = "valid-expect-in-promise";
5944
5926
  const defaultAsyncMatchers$1 = ["toRejectWith", "toResolveWith"];
5945
5927
  const isPromiseChainCall = (node) => {
5946
5928
  if (node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.callee.property)) {
@@ -6072,7 +6054,7 @@ const isVariableAwaitedOrReturned = (variable, context) => {
6072
6054
  return isValueAwaitedOrReturned(variable.id, body, context);
6073
6055
  };
6074
6056
  var valid_expect_in_promise_default = createEslintRule({
6075
- name: RULE_NAME$4,
6057
+ name: RULE_NAME$5,
6076
6058
  meta: {
6077
6059
  docs: { description: "require promises that have expectations in their chain to be valid" },
6078
6060
  messages: { expectInFloatingPromise: "This promise should either be returned or awaited to ensure the expects in its chain are called" },
@@ -6132,7 +6114,7 @@ var valid_expect_in_promise_default = createEslintRule({
6132
6114
 
6133
6115
  //#endregion
6134
6116
  //#region src/rules/valid-expect.ts
6135
- const RULE_NAME$3 = "valid-expect";
6117
+ const RULE_NAME$4 = "valid-expect";
6136
6118
  const defaultAsyncMatchers = ["toReject", "toResolve"];
6137
6119
  /**
6138
6120
  * Async assertions might be called in Promise
@@ -6167,7 +6149,7 @@ const isAcceptableReturnNode = (node, allowReturn) => {
6167
6149
  return [AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.AwaitExpression].includes(node.type);
6168
6150
  };
6169
6151
  var valid_expect_default = createEslintRule({
6170
- name: RULE_NAME$3,
6152
+ name: RULE_NAME$4,
6171
6153
  meta: {
6172
6154
  docs: {
6173
6155
  description: "enforce valid `expect()` usage",
@@ -6360,7 +6342,7 @@ var valid_expect_default = createEslintRule({
6360
6342
 
6361
6343
  //#endregion
6362
6344
  //#region src/rules/valid-title.ts
6363
- const RULE_NAME$2 = "valid-title";
6345
+ const RULE_NAME$3 = "valid-title";
6364
6346
  const trimFXPrefix = (word) => ["f", "x"].includes(word.charAt(0)) ? word.substring(1) : word;
6365
6347
  const quoteStringValue = (node) => node.type === AST_NODE_TYPES.TemplateLiteral ? `\`${node.quasis[0].value.raw}\`` : node.raw;
6366
6348
  const MatcherAndMessageSchema = {
@@ -6399,7 +6381,7 @@ const doesBinaryExpressionContainStringNode = (binaryExp) => {
6399
6381
  return isStringNode(binaryExp.left);
6400
6382
  };
6401
6383
  var valid_title_default = createEslintRule({
6402
- name: RULE_NAME$2,
6384
+ name: RULE_NAME$3,
6403
6385
  meta: {
6404
6386
  docs: {
6405
6387
  description: "enforce valid titles",
@@ -6552,9 +6534,9 @@ var valid_title_default = createEslintRule({
6552
6534
 
6553
6535
  //#endregion
6554
6536
  //#region src/rules/warn-todo.ts
6555
- const RULE_NAME$1 = "warn-todo";
6537
+ const RULE_NAME$2 = "warn-todo";
6556
6538
  var warn_todo_default = createEslintRule({
6557
- name: RULE_NAME$1,
6539
+ name: RULE_NAME$2,
6558
6540
  meta: {
6559
6541
  docs: {
6560
6542
  description: "disallow `.todo` usage",
@@ -6581,7 +6563,7 @@ var warn_todo_default = createEslintRule({
6581
6563
 
6582
6564
  //#endregion
6583
6565
  //#region src/rules/no-unneeded-async-expect-function.ts
6584
- const RULE_NAME = "no-unneeded-async-expect-function";
6566
+ const RULE_NAME$1 = "no-unneeded-async-expect-function";
6585
6567
  const getAwaitedCallExpression = (expression) => {
6586
6568
  if (!expression.async) return null;
6587
6569
  if (expression.type === AST_NODE_TYPES.ArrowFunctionExpression && expression.body.type === AST_NODE_TYPES.AwaitExpression && expression.body.argument.type === AST_NODE_TYPES.CallExpression) return expression.body.argument;
@@ -6591,7 +6573,7 @@ const getAwaitedCallExpression = (expression) => {
6591
6573
  return null;
6592
6574
  };
6593
6575
  var no_unneeded_async_expect_function_default = createEslintRule({
6594
- name: RULE_NAME,
6576
+ name: RULE_NAME$1,
6595
6577
  meta: {
6596
6578
  docs: { description: "Disallow unnecessary async function wrapper for expected promises" },
6597
6579
  fixable: "code",
@@ -6622,6 +6604,42 @@ var no_unneeded_async_expect_function_default = createEslintRule({
6622
6604
  }
6623
6605
  });
6624
6606
 
6607
+ //#endregion
6608
+ //#region src/rules/prefer-to-have-been-called-times.ts
6609
+ const RULE_NAME = "prefer-to-have-been-called-times";
6610
+ var prefer_to_have_been_called_times_default = createEslintRule({
6611
+ name: RULE_NAME,
6612
+ meta: {
6613
+ fixable: "code",
6614
+ docs: { description: "Suggest using `toHaveBeenCalledTimes()`" },
6615
+ messages: { preferMatcher: "Prefer `toHaveBeenCalledTimes`" },
6616
+ type: "suggestion",
6617
+ schema: []
6618
+ },
6619
+ defaultOptions: [],
6620
+ create(context) {
6621
+ return { CallExpression(node) {
6622
+ const vitestFnCall = parseVitestFnCall(node, context);
6623
+ if (vitestFnCall?.type !== "expect") return;
6624
+ const { parent: expect } = vitestFnCall.head.node;
6625
+ if (expect?.type !== AST_NODE_TYPES.CallExpression) return;
6626
+ const { matcher } = vitestFnCall;
6627
+ if (!isSupportedAccessor(matcher, "toHaveLength")) return;
6628
+ const [argument] = expect.arguments;
6629
+ if (argument?.type !== AST_NODE_TYPES.MemberExpression || !isSupportedAccessor(argument.property, "calls")) return;
6630
+ const { object } = argument;
6631
+ if (object.type !== AST_NODE_TYPES.MemberExpression || !isSupportedAccessor(object.property, "mock")) return;
6632
+ context.report({
6633
+ messageId: "preferMatcher",
6634
+ node: matcher,
6635
+ fix(fixer) {
6636
+ return [fixer.removeRange([object.property.range[0] - 1, argument.range[1]]), fixer.replaceTextRange([matcher.parent.object.range[1], matcher.parent.range[1]], ".toHaveBeenCalledTimes")];
6637
+ }
6638
+ });
6639
+ } };
6640
+ }
6641
+ });
6642
+
6625
6643
  //#endregion
6626
6644
  //#region src/rules/index.ts
6627
6645
  const rules = {
@@ -6689,12 +6707,12 @@ const rules = {
6689
6707
  "prefer-to-be-truthy": prefer_to_be_truthy_default,
6690
6708
  "prefer-to-be": prefer_to_be_default,
6691
6709
  "prefer-to-contain": prefer_to_contain_default,
6710
+ "prefer-to-have-been-called-times": prefer_to_have_been_called_times_default,
6692
6711
  "prefer-to-have-length": prefer_to_have_length_default,
6693
6712
  "prefer-todo": prefer_todo_default,
6694
6713
  "prefer-vi-mocked": prefer_vi_mocked_default,
6695
6714
  "require-awaited-expect-poll": require_awaited_expect_poll_default,
6696
6715
  "require-hook": require_hook_default,
6697
- "require-import-vi-mock": require_import_vi_mock_default,
6698
6716
  "require-local-test-context-for-concurrent-snapshots": require_local_test_context_for_concurrent_snapshots_default,
6699
6717
  "require-mock-type-parameters": require_mock_type_parameters_default,
6700
6718
  "require-to-throw-message": require_to_throw_message_default,
@@ -6787,6 +6805,7 @@ const allRules = {
6787
6805
  "prefer-to-be-truthy": "off",
6788
6806
  "prefer-to-be": "warn",
6789
6807
  "prefer-to-contain": "warn",
6808
+ "prefer-to-have-been-called-times": "warn",
6790
6809
  "prefer-to-have-length": "warn",
6791
6810
  "prefer-todo": "warn",
6792
6811
  "prefer-vi-mocked": "warn",
@@ -6799,8 +6818,7 @@ const allRules = {
6799
6818
  "valid-expect-in-promise": "warn",
6800
6819
  "valid-expect": "warn",
6801
6820
  "valid-title": "warn",
6802
- "require-awaited-expect-poll": "warn",
6803
- "require-import-vi-mock": "warn"
6821
+ "require-awaited-expect-poll": "warn"
6804
6822
  };
6805
6823
  const recommendedRules = {
6806
6824
  "expect-expect": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest/eslint-plugin",
3
- "version": "1.5.4",
3
+ "version": "1.6.3",
4
4
  "license": "MIT",
5
5
  "description": "ESLint plugin for Vitest",
6
6
  "repository": "vitest-dev/eslint-plugin-vitest",
@@ -42,7 +42,7 @@
42
42
  "eslint": "^9.37.0",
43
43
  "eslint-config-flat-gitignore": "^2.1.0",
44
44
  "eslint-config-prettier": "^10.1.8",
45
- "eslint-doc-generator": "^2.3.0",
45
+ "eslint-doc-generator": "^2.4.0",
46
46
  "eslint-plugin-eslint-plugin": "^7.0.0",
47
47
  "eslint-remote-tester": "^4.0.3",
48
48
  "eslint-remote-tester-repositories": "^2.0.2",