vitest-auto-spy 3.10.0 → 3.11.0

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/AGENTS.md CHANGED
@@ -1886,8 +1886,9 @@ export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
1886
1886
  | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
1887
1887
  | `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
1888
1888
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
1889
+ | `no-unregistered-inject-spy` | `warn` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
1889
1890
 
1890
- Thirteen rules; two fix on their own, six offer suggestions. `no-mocked-for-spy` only ever touches a
1891
+ Fourteen rules; two fix on their own, six offer suggestions. `no-mocked-for-spy` only ever touches a
1891
1892
  **type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
1892
1893
  meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
1893
1894
  the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
package/README.md CHANGED
@@ -2271,6 +2271,20 @@ seven sites in an 8 673-file workspace, which is small enough to flag at the cur
2271
2271
  and an instance field are deliberately not reported — they run later than the module does — while a
2272
2272
  `static` field is.
2273
2273
 
2274
+ The fourteenth, `no-unregistered-inject-spy`, catches `injectSpy(X)` for a token nothing in the file
2275
+ registered as an auto-spy. What comes back is whatever Angular DI already had — the real service, or
2276
+ an object an imported testing module put there — and nothing complains, because `injectSpy` is
2277
+ declared to return a `Spy<T>`: the helpers are present for `tsc` and absent at run time, so the first
2278
+ `.mockReturnValue(…)` or `.calledWith(…)` throws on a real method. The library warns about exactly
2279
+ this at run time, and that warning is why the rule exists rather than why it is redundant — it does
2280
+ not fail the run, it scrolls past in a suite of a thousand files, and it arrives only for the tests
2281
+ that executed the line; in one consumer monorepo dozens of spec files print it on every CI run and it
2282
+ has never been acted on. Reading it at the cursor needs no types, only the file's own registrations,
2283
+ and the rule stays quiet unless it can read all of them: it needs at least one `provideAutoSpy`, and
2284
+ a spread, an unknown provider factory, `createWithAutoSpies`, `renderShallow` or
2285
+ `TestBed.overrideProvider` silences the file. A token handed a plain `useValue` is left to
2286
+ `prefer-provide-auto-spy` — two rules on one line only teach people to disable both.
2287
+
2274
2288
  ## Editor diagnostics — WebStorm & VS Code
2275
2289
 
2276
2290
  The rules above are worth more while the cursor is still on the line than they are in CI, because
@@ -2280,7 +2294,7 @@ package's own — it needs its ESLint integration switched on.
2280
2294
  ### WebStorm and the other JetBrains IDEs
2281
2295
 
2282
2296
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
2283
- all run ESLint natively, so the twelve rules appear inline, in the **Problems** tool window, and
2297
+ all run ESLint natively, so the fourteen rules appear inline, in the **Problems** tool window, and
2284
2298
  under **Code → Inspect Code** for the whole project.
2285
2299
 
2286
2300
  ```js
@@ -2297,7 +2311,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
2297
2311
  the fixes and suggestions live.
2298
2312
 
2299
2313
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
2300
- and then keep a second copy of twelve rules, in Kotlin, in step with the TypeScript ones.
2314
+ and then keep a second copy of fourteen rules, in Kotlin, in step with the TypeScript ones.
2301
2315
 
2302
2316
  ### VS Code, Cursor, Windsurf, VSCodium
2303
2317
 
@@ -708,6 +708,77 @@ function breaksAnOverride(injection) {
708
708
  return countInSubtree(suite, (node) => isMemberCall(node, TEST_BED, OVERRIDES) && !runsFirst(node, injection, hook), true) > 0;
709
709
  }
710
710
 
711
+ // src/lib/eslint/unregistered-spy.ts
712
+ var PROVIDE_AUTO_SPY = "provideAutoSpy";
713
+ var INJECT_SPY = "injectSpy";
714
+ var AUTO_SPY_FACTORIES = /* @__PURE__ */ new Set(["createAutoMock", "createSpyFromClass", "createMock", "mockDeep"]);
715
+ var OPAQUE_HELPERS = /* @__PURE__ */ new Set(["createWithAutoSpies", "renderShallow"]);
716
+ function emptyRegistrations() {
717
+ return { autoSpies: /* @__PURE__ */ new Set(), opaqueTokens: /* @__PURE__ */ new Set(), injections: [], sawProvideAutoSpy: false, opaque: false };
718
+ }
719
+ function calleeName(node) {
720
+ return isIdentifier(node.callee) ? node.callee.name : void 0;
721
+ }
722
+ function isOverrideProvider(node) {
723
+ const { callee } = node;
724
+ return isMemberExpression(callee) && isIdentifier(callee.object) && callee.object.name === "TestBed" && isIdentifier(callee.property) && callee.property.name === "overrideProvider";
725
+ }
726
+ function readCall(context, node, tally) {
727
+ const name = calleeName(node);
728
+ const [first] = node.arguments;
729
+ if (isOverrideProvider(node) || name !== void 0 && OPAQUE_HELPERS.has(name)) {
730
+ tally.opaque = true;
731
+ return;
732
+ }
733
+ if (first === void 0) {
734
+ return;
735
+ }
736
+ if (name === PROVIDE_AUTO_SPY) {
737
+ tally.sawProvideAutoSpy = true;
738
+ tally.autoSpies.add(context.sourceCode.getText(first));
739
+ return;
740
+ }
741
+ if (name === INJECT_SPY) {
742
+ tally.injections.push({ node, token: context.sourceCode.getText(first) });
743
+ }
744
+ }
745
+ function isAutoSpyValue(value) {
746
+ return isCallExpression(value) && isIdentifier(value.callee) && AUTO_SPY_FACTORIES.has(value.callee.name);
747
+ }
748
+ function readProviderObject(context, element, tally) {
749
+ const provide = findProperty(element, "provide");
750
+ if (provide === void 0) {
751
+ tally.opaque = true;
752
+ return;
753
+ }
754
+ const token = context.sourceCode.getText(propertyValue(provide));
755
+ const useValue = findProperty(element, "useValue");
756
+ if (useValue !== void 0 && isAutoSpyValue(propertyValue(useValue))) {
757
+ tally.autoSpies.add(token);
758
+ return;
759
+ }
760
+ tally.opaqueTokens.add(token);
761
+ }
762
+ function readProviders(context, array, tally) {
763
+ for (const element of array.elements) {
764
+ if (element === null) {
765
+ tally.opaque = true;
766
+ continue;
767
+ }
768
+ if (isObjectExpression(element)) {
769
+ readProviderObject(context, element, tally);
770
+ } else if (!isCallExpression(element) || calleeName(element) !== PROVIDE_AUTO_SPY) {
771
+ tally.opaque = true;
772
+ }
773
+ }
774
+ }
775
+ function unregisteredInjections(tally) {
776
+ if (tally.opaque || !tally.sawProvideAutoSpy) {
777
+ return [];
778
+ }
779
+ return tally.injections.filter(({ token }) => !tally.autoSpies.has(token) && !tally.opaqueTokens.has(token));
780
+ }
781
+
711
782
  // src/lib/eslint/rules.ts
712
783
  var README = "https://github.com/ASDAlexey/vitest-auto-spy#how-to-mock";
713
784
  function defineRule(options) {
@@ -1116,6 +1187,29 @@ var noImportTimeSpread = defineRule({
1116
1187
  }
1117
1188
  })
1118
1189
  });
1190
+ var noUnregisteredInjectSpy = defineRule({
1191
+ anchor: "-a-service-behind-angular-di",
1192
+ description: "Do not read a token with injectSpy unless this file registered it as an auto-spy",
1193
+ messages: {
1194
+ noUnregisteredInjectSpy: "Nothing in this file registers `{{token}}` as an auto-spy, so this hands back whatever Angular DI already had \u2014 the real service, or a hand-rolled object some imported module provides. The line still compiles and the spec still runs: `injectSpy` types the result as a spy, so the helpers are there for the compiler and absent at run time, and the first `.mockReturnValue(\u2026)` or `.calledWith(\u2026)` throws on a real method. Add `provideAutoSpy({{token}})` to the providers, or read the real implementation with `TestBed.inject({{token}})` and say so. Nothing is reported from a file whose providers this cannot read in full \u2014 a spread, an unknown provider factory, `createWithAutoSpies` or `TestBed.overrideProvider` all silence it."
1195
+ },
1196
+ create: (context) => {
1197
+ const tally = emptyRegistrations();
1198
+ return {
1199
+ CallExpression: (node) => {
1200
+ readCall(context, node, tally);
1201
+ },
1202
+ 'Property[key.name="providers"] > ArrayExpression': (node) => {
1203
+ readProviders(context, node, tally);
1204
+ },
1205
+ "Program:exit": () => {
1206
+ unregisteredInjections(tally).forEach(({ node, token }) => {
1207
+ context.report({ node, messageId: "noUnregisteredInjectSpy", data: { token } });
1208
+ });
1209
+ }
1210
+ };
1211
+ }
1212
+ });
1119
1213
  var rules = {
1120
1214
  "prefer-provide-auto-spy": preferProvideAutoSpy,
1121
1215
  "prefer-create-spy-from-class": preferCreateSpyFromClass,
@@ -1129,7 +1223,8 @@ var rules = {
1129
1223
  "no-floating-assertion": noFloatingAssertion,
1130
1224
  "no-overridden-provider": noOverriddenProvider,
1131
1225
  "no-inject-before-override": noInjectBeforeOverride,
1132
- "no-import-time-spread": noImportTimeSpread
1226
+ "no-import-time-spread": noImportTimeSpread,
1227
+ "no-unregistered-inject-spy": noUnregisteredInjectSpy
1133
1228
  };
1134
1229
 
1135
1230
  // src/eslint-plugin.ts
@@ -1147,7 +1242,8 @@ var recommendedRules = {
1147
1242
  [`${PLUGIN_NAME}/no-floating-assertion`]: "error",
1148
1243
  [`${PLUGIN_NAME}/no-overridden-provider`]: "error",
1149
1244
  [`${PLUGIN_NAME}/no-inject-before-override`]: "warn",
1150
- [`${PLUGIN_NAME}/no-import-time-spread`]: "error"
1245
+ [`${PLUGIN_NAME}/no-import-time-spread`]: "error",
1246
+ [`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "warn"
1151
1247
  };
1152
1248
  var plugin = {
1153
1249
  rules,
@@ -706,6 +706,77 @@ function breaksAnOverride(injection) {
706
706
  return countInSubtree(suite, (node) => isMemberCall(node, TEST_BED, OVERRIDES) && !runsFirst(node, injection, hook), true) > 0;
707
707
  }
708
708
 
709
+ // src/lib/eslint/unregistered-spy.ts
710
+ var PROVIDE_AUTO_SPY = "provideAutoSpy";
711
+ var INJECT_SPY = "injectSpy";
712
+ var AUTO_SPY_FACTORIES = /* @__PURE__ */ new Set(["createAutoMock", "createSpyFromClass", "createMock", "mockDeep"]);
713
+ var OPAQUE_HELPERS = /* @__PURE__ */ new Set(["createWithAutoSpies", "renderShallow"]);
714
+ function emptyRegistrations() {
715
+ return { autoSpies: /* @__PURE__ */ new Set(), opaqueTokens: /* @__PURE__ */ new Set(), injections: [], sawProvideAutoSpy: false, opaque: false };
716
+ }
717
+ function calleeName(node) {
718
+ return isIdentifier(node.callee) ? node.callee.name : void 0;
719
+ }
720
+ function isOverrideProvider(node) {
721
+ const { callee } = node;
722
+ return isMemberExpression(callee) && isIdentifier(callee.object) && callee.object.name === "TestBed" && isIdentifier(callee.property) && callee.property.name === "overrideProvider";
723
+ }
724
+ function readCall(context, node, tally) {
725
+ const name = calleeName(node);
726
+ const [first] = node.arguments;
727
+ if (isOverrideProvider(node) || name !== void 0 && OPAQUE_HELPERS.has(name)) {
728
+ tally.opaque = true;
729
+ return;
730
+ }
731
+ if (first === void 0) {
732
+ return;
733
+ }
734
+ if (name === PROVIDE_AUTO_SPY) {
735
+ tally.sawProvideAutoSpy = true;
736
+ tally.autoSpies.add(context.sourceCode.getText(first));
737
+ return;
738
+ }
739
+ if (name === INJECT_SPY) {
740
+ tally.injections.push({ node, token: context.sourceCode.getText(first) });
741
+ }
742
+ }
743
+ function isAutoSpyValue(value) {
744
+ return isCallExpression(value) && isIdentifier(value.callee) && AUTO_SPY_FACTORIES.has(value.callee.name);
745
+ }
746
+ function readProviderObject(context, element, tally) {
747
+ const provide = findProperty(element, "provide");
748
+ if (provide === void 0) {
749
+ tally.opaque = true;
750
+ return;
751
+ }
752
+ const token = context.sourceCode.getText(propertyValue(provide));
753
+ const useValue = findProperty(element, "useValue");
754
+ if (useValue !== void 0 && isAutoSpyValue(propertyValue(useValue))) {
755
+ tally.autoSpies.add(token);
756
+ return;
757
+ }
758
+ tally.opaqueTokens.add(token);
759
+ }
760
+ function readProviders(context, array, tally) {
761
+ for (const element of array.elements) {
762
+ if (element === null) {
763
+ tally.opaque = true;
764
+ continue;
765
+ }
766
+ if (isObjectExpression(element)) {
767
+ readProviderObject(context, element, tally);
768
+ } else if (!isCallExpression(element) || calleeName(element) !== PROVIDE_AUTO_SPY) {
769
+ tally.opaque = true;
770
+ }
771
+ }
772
+ }
773
+ function unregisteredInjections(tally) {
774
+ if (tally.opaque || !tally.sawProvideAutoSpy) {
775
+ return [];
776
+ }
777
+ return tally.injections.filter(({ token }) => !tally.autoSpies.has(token) && !tally.opaqueTokens.has(token));
778
+ }
779
+
709
780
  // src/lib/eslint/rules.ts
710
781
  var README = "https://github.com/ASDAlexey/vitest-auto-spy#how-to-mock";
711
782
  function defineRule(options) {
@@ -1114,6 +1185,29 @@ var noImportTimeSpread = defineRule({
1114
1185
  }
1115
1186
  })
1116
1187
  });
1188
+ var noUnregisteredInjectSpy = defineRule({
1189
+ anchor: "-a-service-behind-angular-di",
1190
+ description: "Do not read a token with injectSpy unless this file registered it as an auto-spy",
1191
+ messages: {
1192
+ noUnregisteredInjectSpy: "Nothing in this file registers `{{token}}` as an auto-spy, so this hands back whatever Angular DI already had \u2014 the real service, or a hand-rolled object some imported module provides. The line still compiles and the spec still runs: `injectSpy` types the result as a spy, so the helpers are there for the compiler and absent at run time, and the first `.mockReturnValue(\u2026)` or `.calledWith(\u2026)` throws on a real method. Add `provideAutoSpy({{token}})` to the providers, or read the real implementation with `TestBed.inject({{token}})` and say so. Nothing is reported from a file whose providers this cannot read in full \u2014 a spread, an unknown provider factory, `createWithAutoSpies` or `TestBed.overrideProvider` all silence it."
1193
+ },
1194
+ create: (context) => {
1195
+ const tally = emptyRegistrations();
1196
+ return {
1197
+ CallExpression: (node) => {
1198
+ readCall(context, node, tally);
1199
+ },
1200
+ 'Property[key.name="providers"] > ArrayExpression': (node) => {
1201
+ readProviders(context, node, tally);
1202
+ },
1203
+ "Program:exit": () => {
1204
+ unregisteredInjections(tally).forEach(({ node, token }) => {
1205
+ context.report({ node, messageId: "noUnregisteredInjectSpy", data: { token } });
1206
+ });
1207
+ }
1208
+ };
1209
+ }
1210
+ });
1117
1211
  var rules = {
1118
1212
  "prefer-provide-auto-spy": preferProvideAutoSpy,
1119
1213
  "prefer-create-spy-from-class": preferCreateSpyFromClass,
@@ -1127,7 +1221,8 @@ var rules = {
1127
1221
  "no-floating-assertion": noFloatingAssertion,
1128
1222
  "no-overridden-provider": noOverriddenProvider,
1129
1223
  "no-inject-before-override": noInjectBeforeOverride,
1130
- "no-import-time-spread": noImportTimeSpread
1224
+ "no-import-time-spread": noImportTimeSpread,
1225
+ "no-unregistered-inject-spy": noUnregisteredInjectSpy
1131
1226
  };
1132
1227
 
1133
1228
  // src/eslint-plugin.ts
@@ -1145,7 +1240,8 @@ var recommendedRules = {
1145
1240
  [`${PLUGIN_NAME}/no-floating-assertion`]: "error",
1146
1241
  [`${PLUGIN_NAME}/no-overridden-provider`]: "error",
1147
1242
  [`${PLUGIN_NAME}/no-inject-before-override`]: "warn",
1148
- [`${PLUGIN_NAME}/no-import-time-spread`]: "error"
1243
+ [`${PLUGIN_NAME}/no-import-time-spread`]: "error",
1244
+ [`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "warn"
1149
1245
  };
1150
1246
  var plugin = {
1151
1247
  rules,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun and node:test, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",
@@ -238,7 +238,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
238
238
  Most of this library's guarantees are type-level, so a green run that does not type-check is not
239
239
  done. Report failures with their output rather than describing them as passing.
240
240
 
241
- **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The thirteen rules in
241
+ **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The fourteen rules in
242
242
  `vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
243
243
  to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
244
244
  evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —