oxlint-plugin-arch 0.7.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ - Add `limits.maxTypeFileLines` (default `1000`): the preset applies this `max-lines` limit to `*.types.ts` files in app roots instead of `limits.maxFileLines`. Other files keep `maxFileLines`.
6
+
7
+ ## 0.8.0
8
+
9
+ - Add `tests.mocking` to choose which test-double APIs the preset bans in test files: `'modules'` (default, unchanged) or `'strict'`, which also bans `spyOn`, `stubGlobal` and `stubEnv` outside `*.driver.test.ts` and asks for a fake driver. The object form adds `deny` API patterns and per-file `allow` exceptions.
10
+ - Add `deny`, `allow` and `checkDestructured` options to `dillon-anti-slop/no-module-mocking`. Invalid API patterns throw. Without options it reports the same calls as before.
11
+
3
12
  ## 0.7.0
4
13
 
5
14
  - Update bundled plugins: `@oxlint/plugins` and `oxlint-plugin-eslint` 1.89, `oxlint-tailwindcss` 1.16, `eslint-plugin-perfectionist` 5.13, `eslint-plugin-tsdoc` 0.5.4 and `@shadcn/lint` 0.2. `@shadcn/lint` now prints a notice when a project's `cn` is older than 0.4 and lints with its bundled `cn` instead.
package/README.md CHANGED
@@ -230,7 +230,11 @@ Rules the preset uses to keep its policies from overwriting each other. You can
230
230
 
231
231
  ### `dillon-anti-slop/*`
232
232
 
233
- 15 vendored [anti-slop](https://github.com/dmmulroy/anti-slop) rules by Dillon Mulroy. [List](docs/rules/dillon-anti-slop.md).
233
+ 15 vendored [anti-slop](https://github.com/dmmulroy/anti-slop) rules by Dillon Mulroy. [List](docs/rules/dillon-anti-slop.md). Rules with local options have their own page:
234
+
235
+ | Rule | What it enforces |
236
+ | --- | --- |
237
+ | [`no-module-mocking`](docs/rules/dillon-anti-slop/no-module-mocking.md) | No `vi.mock` or `jest.mock`. Options also ban stubs such as `vi.spyOn`. |
234
238
 
235
239
  ## ⚡ Benchmarks
236
240
 
@@ -1,4 +1,4 @@
1
- import type { PresetRules, PresetPolicyName } from '../types/preset.types.ts';
1
+ import type { PresetRules, PresetPolicyName, PresetTestMockingException, PresetTestMockingPreset } from '../types/preset.types.ts';
2
2
  export declare const presetExtensions = "*.{ts,tsx,mts,cts}";
3
3
  export declare const presetPolicyOrder: PresetPolicyName[];
4
4
  export declare const presetCommonRules: PresetRules;
@@ -18,6 +18,10 @@ export declare const presetTailwindRules: {
18
18
  export declare const presetImportRules: PresetRules;
19
19
  export declare const presetNamingRules: PresetRules;
20
20
  export declare const presetTestRules: PresetRules;
21
+ /** Test-double APIs each `tests.mocking` preset bans, for `vi` and `jest`. */
22
+ export declare const presetTestMockingApis: Record<PresetTestMockingPreset, string[]>;
23
+ export declare const presetTestMockingAllow: Record<PresetTestMockingPreset, PresetTestMockingException[]>;
24
+ export declare const presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
21
25
  /** HTTP methods a Next `route.ts` handler may export. */
22
26
  export declare const presetNextHandlerMethods: string[];
23
27
  /** Next route segment config exports, shared by pages, layouts and route handlers. */
package/dist/index.js CHANGED
@@ -749,6 +749,11 @@ function layoutNewline(text) {
749
749
  ` : `
750
750
  `;
751
751
  }
752
+ // src/utils/helpers/mocking.ts
753
+ function mockingApiPatternsAssert(patterns) {
754
+ if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
755
+ throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
756
+ }
752
757
  // src/utils/helpers/options.ts
753
758
  function optionsFirst(context, fallback) {
754
759
  return context.options[0] ?? fallback;
@@ -1,4 +1,4 @@
1
1
  export { default as reactModulesPreset } from './react-modules-preset.ts';
2
2
  export { default as nextReactModulesPreset } from './next-react-modules-preset.ts';
3
3
  export { default as tanstackStartReactModulesPreset } from './tanstack-start-react-modules-preset.ts';
4
- export type { PresetOptions, PresetArchitecture, PresetArchitectureEntry, PresetFolderLayout, PresetFormattingOptions, PresetFramework, PresetNextOptions, PresetTanstackStartOptions, PresetLimitsOptions, PresetModulesOptions, PresetImportsOptions, PresetOrpcOptions, PresetSqlOptions, PresetFormsOptions, PresetReactOptions, PresetTestsOptions, PresetPolicyOptions, PresetPolicy, PresetPolicyName, PresetTailwindOptions, PresetShadcnOptions, } from '../types/preset.types.ts';
4
+ export type { PresetOptions, PresetArchitecture, PresetArchitectureEntry, PresetFolderLayout, PresetFormattingOptions, PresetFramework, PresetNextOptions, PresetTanstackStartOptions, PresetLimitsOptions, PresetModulesOptions, PresetImportsOptions, PresetOrpcOptions, PresetSqlOptions, PresetFormsOptions, PresetReactOptions, PresetTestsOptions, PresetTestMockingApi, PresetTestMockingException, PresetTestMockingOptions, PresetTestMockingPreset, PresetPolicyOptions, PresetPolicy, PresetPolicyName, PresetTailwindOptions, PresetShadcnOptions, } from '../types/preset.types.ts';
@@ -418,6 +418,16 @@ var presetTestRules = {
418
418
  "vitest/no-conditional-expect": "error",
419
419
  "vitest/require-mock-type-parameters": "error"
420
420
  };
421
+ var presetModuleMockingApis = ["*.mock", "*.doMock", "*.unstable_mockModule"];
422
+ var presetTestMockingApis = {
423
+ modules: presetModuleMockingApis,
424
+ strict: [...presetModuleMockingApis, "*.spyOn", "*.stubGlobal", "*.stubEnv"]
425
+ };
426
+ var presetTestMockingAllow = {
427
+ modules: [],
428
+ strict: [{ files: ["**/*.driver.test.ts"], apis: ["*"] }]
429
+ };
430
+ var presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
421
431
  var presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
422
432
  var presetNextSegmentConfig = [
423
433
  "generateStaticParams",
@@ -949,6 +959,12 @@ function presetModulesConfig(context) {
949
959
  return policies;
950
960
  }
951
961
 
962
+ // src/utils/helpers/mocking.ts
963
+ function mockingApiPatternsAssert(patterns) {
964
+ if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
965
+ throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
966
+ }
967
+
952
968
  // src/configs/tests.ts
953
969
  function presetTestsConfig(context) {
954
970
  const { backendRoots, testFiles, baseImports, options } = context;
@@ -961,11 +977,30 @@ function presetTestsConfig(context) {
961
977
  throw new Error("Additional test functions must be literal identifiers or dotted names.");
962
978
  if (assertionFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[\w$*]+)*$/.test(name)))
963
979
  throw new Error("Assertion functions must be rooted dotted names or patterns.");
980
+ const mocking = typeof options.tests?.mocking === "string" ? { preset: options.tests.mocking } : options.tests?.mocking ?? {};
981
+ const mockingPreset = mocking.preset ?? "modules";
982
+ if (mockingPreset !== "modules" && mockingPreset !== "strict")
983
+ throw new Error("Unknown test mocking preset.");
984
+ const mockingDeny = [...mocking.deny ?? [], ...presetTestMockingApis[mockingPreset]].map((entry) => {
985
+ if (typeof entry !== "string")
986
+ return entry;
987
+ return mockingPreset === "strict" ? { api: entry, message: presetStrictMockingMessage } : entry;
988
+ });
989
+ const mockingAllow = [...presetTestMockingAllow[mockingPreset], ...mocking.allow ?? []].sort((left, right) => Number(left.apis.includes("*")) - Number(right.apis.includes("*")));
990
+ mockingApiPatternsAssert([
991
+ ...mockingDeny.map((entry) => typeof entry === "string" ? entry : entry.api),
992
+ ...mockingAllow.flatMap((entry) => entry.apis)
993
+ ]);
994
+ const mockingCustomized = mockingPreset !== "modules" || (mocking.deny ?? []).length > 0;
995
+ const mockingRule = (allow) => [
996
+ "error",
997
+ { deny: mockingDeny, allow: ["*.fn", ...allow], checkDestructured: mockingCustomized }
998
+ ];
964
999
  const policies = {};
965
1000
  policies.tests = [
966
1001
  presetOverride(testFiles, {
967
1002
  ...presetTestRules,
968
- "dillon-anti-slop/no-module-mocking": "error",
1003
+ "dillon-anti-slop/no-module-mocking": mockingCustomized ? mockingRule([]) : "error",
969
1004
  "modules/test-modifiers": [
970
1005
  "error",
971
1006
  { additionalTestFunctions: options.tests?.additionalTestFunctions ?? [] }
@@ -998,7 +1033,10 @@ function presetTestsConfig(context) {
998
1033
  }
999
1034
  ]
1000
1035
  } : {}
1001
- })
1036
+ }),
1037
+ ...mockingAllow.map((entry) => presetOverride(entry.files, {
1038
+ "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis)
1039
+ }))
1002
1040
  ];
1003
1041
  return policies;
1004
1042
  }
@@ -1501,9 +1539,10 @@ function presetResolveContext(options) {
1501
1539
 
1502
1540
  // src/services/preset-compose.ts
1503
1541
  function presetCompose(context, policies, plugins) {
1504
- const { options, level, safetyFiles, ui, generated } = context;
1542
+ const { options, level, app, safetyFiles, ui, generated } = context;
1505
1543
  const severities = options.ruleSeverities ?? {};
1506
1544
  const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
1545
+ const maxLines = (max) => levels({ "max-lines": ["error", { max, skipBlankLines: true, skipComments: true }] });
1507
1546
  const overrideList = [];
1508
1547
  for (const name of presetPolicyOrder) {
1509
1548
  const setting = options.policies?.[name];
@@ -1520,9 +1559,9 @@ function presetCompose(context, policies, plugins) {
1520
1559
  }
1521
1560
  }
1522
1561
  overrideList.push(presetOverride(safetyFiles, levels({
1523
- "max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1562
+ ...maxLines(options.limits?.maxFileLines ?? 400),
1524
1563
  complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
1525
- }), ui));
1564
+ }), ui), presetOverride(presetScopes(app, "**/*.types.ts"), maxLines(options.limits?.maxTypeFileLines ?? 1000), ui));
1526
1565
  const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
1527
1566
  const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
1528
1567
  for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {
@@ -418,6 +418,16 @@ var presetTestRules = {
418
418
  "vitest/no-conditional-expect": "error",
419
419
  "vitest/require-mock-type-parameters": "error"
420
420
  };
421
+ var presetModuleMockingApis = ["*.mock", "*.doMock", "*.unstable_mockModule"];
422
+ var presetTestMockingApis = {
423
+ modules: presetModuleMockingApis,
424
+ strict: [...presetModuleMockingApis, "*.spyOn", "*.stubGlobal", "*.stubEnv"]
425
+ };
426
+ var presetTestMockingAllow = {
427
+ modules: [],
428
+ strict: [{ files: ["**/*.driver.test.ts"], apis: ["*"] }]
429
+ };
430
+ var presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
421
431
  var presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
422
432
  var presetNextSegmentConfig = [
423
433
  "generateStaticParams",
@@ -949,6 +959,12 @@ function presetModulesConfig(context) {
949
959
  return policies;
950
960
  }
951
961
 
962
+ // src/utils/helpers/mocking.ts
963
+ function mockingApiPatternsAssert(patterns) {
964
+ if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
965
+ throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
966
+ }
967
+
952
968
  // src/configs/tests.ts
953
969
  function presetTestsConfig(context) {
954
970
  const { backendRoots, testFiles, baseImports, options } = context;
@@ -961,11 +977,30 @@ function presetTestsConfig(context) {
961
977
  throw new Error("Additional test functions must be literal identifiers or dotted names.");
962
978
  if (assertionFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[\w$*]+)*$/.test(name)))
963
979
  throw new Error("Assertion functions must be rooted dotted names or patterns.");
980
+ const mocking = typeof options.tests?.mocking === "string" ? { preset: options.tests.mocking } : options.tests?.mocking ?? {};
981
+ const mockingPreset = mocking.preset ?? "modules";
982
+ if (mockingPreset !== "modules" && mockingPreset !== "strict")
983
+ throw new Error("Unknown test mocking preset.");
984
+ const mockingDeny = [...mocking.deny ?? [], ...presetTestMockingApis[mockingPreset]].map((entry) => {
985
+ if (typeof entry !== "string")
986
+ return entry;
987
+ return mockingPreset === "strict" ? { api: entry, message: presetStrictMockingMessage } : entry;
988
+ });
989
+ const mockingAllow = [...presetTestMockingAllow[mockingPreset], ...mocking.allow ?? []].sort((left, right) => Number(left.apis.includes("*")) - Number(right.apis.includes("*")));
990
+ mockingApiPatternsAssert([
991
+ ...mockingDeny.map((entry) => typeof entry === "string" ? entry : entry.api),
992
+ ...mockingAllow.flatMap((entry) => entry.apis)
993
+ ]);
994
+ const mockingCustomized = mockingPreset !== "modules" || (mocking.deny ?? []).length > 0;
995
+ const mockingRule = (allow) => [
996
+ "error",
997
+ { deny: mockingDeny, allow: ["*.fn", ...allow], checkDestructured: mockingCustomized }
998
+ ];
964
999
  const policies = {};
965
1000
  policies.tests = [
966
1001
  presetOverride(testFiles, {
967
1002
  ...presetTestRules,
968
- "dillon-anti-slop/no-module-mocking": "error",
1003
+ "dillon-anti-slop/no-module-mocking": mockingCustomized ? mockingRule([]) : "error",
969
1004
  "modules/test-modifiers": [
970
1005
  "error",
971
1006
  { additionalTestFunctions: options.tests?.additionalTestFunctions ?? [] }
@@ -998,7 +1033,10 @@ function presetTestsConfig(context) {
998
1033
  }
999
1034
  ]
1000
1035
  } : {}
1001
- })
1036
+ }),
1037
+ ...mockingAllow.map((entry) => presetOverride(entry.files, {
1038
+ "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis)
1039
+ }))
1002
1040
  ];
1003
1041
  return policies;
1004
1042
  }
@@ -1501,9 +1539,10 @@ function presetResolveContext(options) {
1501
1539
 
1502
1540
  // src/services/preset-compose.ts
1503
1541
  function presetCompose(context, policies, plugins) {
1504
- const { options, level, safetyFiles, ui, generated } = context;
1542
+ const { options, level, app, safetyFiles, ui, generated } = context;
1505
1543
  const severities = options.ruleSeverities ?? {};
1506
1544
  const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
1545
+ const maxLines = (max) => levels({ "max-lines": ["error", { max, skipBlankLines: true, skipComments: true }] });
1507
1546
  const overrideList = [];
1508
1547
  for (const name of presetPolicyOrder) {
1509
1548
  const setting = options.policies?.[name];
@@ -1520,9 +1559,9 @@ function presetCompose(context, policies, plugins) {
1520
1559
  }
1521
1560
  }
1522
1561
  overrideList.push(presetOverride(safetyFiles, levels({
1523
- "max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1562
+ ...maxLines(options.limits?.maxFileLines ?? 400),
1524
1563
  complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
1525
- }), ui));
1564
+ }), ui), presetOverride(presetScopes(app, "**/*.types.ts"), maxLines(options.limits?.maxTypeFileLines ?? 1000), ui));
1526
1565
  const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
1527
1566
  const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
1528
1567
  for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {
@@ -418,6 +418,16 @@ var presetTestRules = {
418
418
  "vitest/no-conditional-expect": "error",
419
419
  "vitest/require-mock-type-parameters": "error"
420
420
  };
421
+ var presetModuleMockingApis = ["*.mock", "*.doMock", "*.unstable_mockModule"];
422
+ var presetTestMockingApis = {
423
+ modules: presetModuleMockingApis,
424
+ strict: [...presetModuleMockingApis, "*.spyOn", "*.stubGlobal", "*.stubEnv"]
425
+ };
426
+ var presetTestMockingAllow = {
427
+ modules: [],
428
+ strict: [{ files: ["**/*.driver.test.ts"], apis: ["*"] }]
429
+ };
430
+ var presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
421
431
  var presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
422
432
  var presetNextSegmentConfig = [
423
433
  "generateStaticParams",
@@ -949,6 +959,12 @@ function presetModulesConfig(context) {
949
959
  return policies;
950
960
  }
951
961
 
962
+ // src/utils/helpers/mocking.ts
963
+ function mockingApiPatternsAssert(patterns) {
964
+ if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
965
+ throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
966
+ }
967
+
952
968
  // src/configs/tests.ts
953
969
  function presetTestsConfig(context) {
954
970
  const { backendRoots, testFiles, baseImports, options } = context;
@@ -961,11 +977,30 @@ function presetTestsConfig(context) {
961
977
  throw new Error("Additional test functions must be literal identifiers or dotted names.");
962
978
  if (assertionFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[\w$*]+)*$/.test(name)))
963
979
  throw new Error("Assertion functions must be rooted dotted names or patterns.");
980
+ const mocking = typeof options.tests?.mocking === "string" ? { preset: options.tests.mocking } : options.tests?.mocking ?? {};
981
+ const mockingPreset = mocking.preset ?? "modules";
982
+ if (mockingPreset !== "modules" && mockingPreset !== "strict")
983
+ throw new Error("Unknown test mocking preset.");
984
+ const mockingDeny = [...mocking.deny ?? [], ...presetTestMockingApis[mockingPreset]].map((entry) => {
985
+ if (typeof entry !== "string")
986
+ return entry;
987
+ return mockingPreset === "strict" ? { api: entry, message: presetStrictMockingMessage } : entry;
988
+ });
989
+ const mockingAllow = [...presetTestMockingAllow[mockingPreset], ...mocking.allow ?? []].sort((left, right) => Number(left.apis.includes("*")) - Number(right.apis.includes("*")));
990
+ mockingApiPatternsAssert([
991
+ ...mockingDeny.map((entry) => typeof entry === "string" ? entry : entry.api),
992
+ ...mockingAllow.flatMap((entry) => entry.apis)
993
+ ]);
994
+ const mockingCustomized = mockingPreset !== "modules" || (mocking.deny ?? []).length > 0;
995
+ const mockingRule = (allow) => [
996
+ "error",
997
+ { deny: mockingDeny, allow: ["*.fn", ...allow], checkDestructured: mockingCustomized }
998
+ ];
964
999
  const policies = {};
965
1000
  policies.tests = [
966
1001
  presetOverride(testFiles, {
967
1002
  ...presetTestRules,
968
- "dillon-anti-slop/no-module-mocking": "error",
1003
+ "dillon-anti-slop/no-module-mocking": mockingCustomized ? mockingRule([]) : "error",
969
1004
  "modules/test-modifiers": [
970
1005
  "error",
971
1006
  { additionalTestFunctions: options.tests?.additionalTestFunctions ?? [] }
@@ -998,7 +1033,10 @@ function presetTestsConfig(context) {
998
1033
  }
999
1034
  ]
1000
1035
  } : {}
1001
- })
1036
+ }),
1037
+ ...mockingAllow.map((entry) => presetOverride(entry.files, {
1038
+ "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis)
1039
+ }))
1002
1040
  ];
1003
1041
  return policies;
1004
1042
  }
@@ -1501,9 +1539,10 @@ function presetResolveContext(options) {
1501
1539
 
1502
1540
  // src/services/preset-compose.ts
1503
1541
  function presetCompose(context, policies, plugins) {
1504
- const { options, level, safetyFiles, ui, generated } = context;
1542
+ const { options, level, app, safetyFiles, ui, generated } = context;
1505
1543
  const severities = options.ruleSeverities ?? {};
1506
1544
  const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
1545
+ const maxLines = (max) => levels({ "max-lines": ["error", { max, skipBlankLines: true, skipComments: true }] });
1507
1546
  const overrideList = [];
1508
1547
  for (const name of presetPolicyOrder) {
1509
1548
  const setting = options.policies?.[name];
@@ -1520,9 +1559,9 @@ function presetCompose(context, policies, plugins) {
1520
1559
  }
1521
1560
  }
1522
1561
  overrideList.push(presetOverride(safetyFiles, levels({
1523
- "max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1562
+ ...maxLines(options.limits?.maxFileLines ?? 400),
1524
1563
  complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
1525
- }), ui));
1564
+ }), ui), presetOverride(presetScopes(app, "**/*.types.ts"), maxLines(options.limits?.maxTypeFileLines ?? 1000), ui));
1526
1565
  const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
1527
1566
  const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
1528
1567
  for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {
@@ -418,6 +418,16 @@ var presetTestRules = {
418
418
  "vitest/no-conditional-expect": "error",
419
419
  "vitest/require-mock-type-parameters": "error"
420
420
  };
421
+ var presetModuleMockingApis = ["*.mock", "*.doMock", "*.unstable_mockModule"];
422
+ var presetTestMockingApis = {
423
+ modules: presetModuleMockingApis,
424
+ strict: [...presetModuleMockingApis, "*.spyOn", "*.stubGlobal", "*.stubEnv"]
425
+ };
426
+ var presetTestMockingAllow = {
427
+ modules: [],
428
+ strict: [{ files: ["**/*.driver.test.ts"], apis: ["*"] }]
429
+ };
430
+ var presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
421
431
  var presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
422
432
  var presetNextSegmentConfig = [
423
433
  "generateStaticParams",
@@ -949,6 +959,12 @@ function presetModulesConfig(context) {
949
959
  return policies;
950
960
  }
951
961
 
962
+ // src/utils/helpers/mocking.ts
963
+ function mockingApiPatternsAssert(patterns) {
964
+ if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
965
+ throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
966
+ }
967
+
952
968
  // src/configs/tests.ts
953
969
  function presetTestsConfig(context) {
954
970
  const { backendRoots, testFiles, baseImports, options } = context;
@@ -961,11 +977,30 @@ function presetTestsConfig(context) {
961
977
  throw new Error("Additional test functions must be literal identifiers or dotted names.");
962
978
  if (assertionFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[\w$*]+)*$/.test(name)))
963
979
  throw new Error("Assertion functions must be rooted dotted names or patterns.");
980
+ const mocking = typeof options.tests?.mocking === "string" ? { preset: options.tests.mocking } : options.tests?.mocking ?? {};
981
+ const mockingPreset = mocking.preset ?? "modules";
982
+ if (mockingPreset !== "modules" && mockingPreset !== "strict")
983
+ throw new Error("Unknown test mocking preset.");
984
+ const mockingDeny = [...mocking.deny ?? [], ...presetTestMockingApis[mockingPreset]].map((entry) => {
985
+ if (typeof entry !== "string")
986
+ return entry;
987
+ return mockingPreset === "strict" ? { api: entry, message: presetStrictMockingMessage } : entry;
988
+ });
989
+ const mockingAllow = [...presetTestMockingAllow[mockingPreset], ...mocking.allow ?? []].sort((left, right) => Number(left.apis.includes("*")) - Number(right.apis.includes("*")));
990
+ mockingApiPatternsAssert([
991
+ ...mockingDeny.map((entry) => typeof entry === "string" ? entry : entry.api),
992
+ ...mockingAllow.flatMap((entry) => entry.apis)
993
+ ]);
994
+ const mockingCustomized = mockingPreset !== "modules" || (mocking.deny ?? []).length > 0;
995
+ const mockingRule = (allow) => [
996
+ "error",
997
+ { deny: mockingDeny, allow: ["*.fn", ...allow], checkDestructured: mockingCustomized }
998
+ ];
964
999
  const policies = {};
965
1000
  policies.tests = [
966
1001
  presetOverride(testFiles, {
967
1002
  ...presetTestRules,
968
- "dillon-anti-slop/no-module-mocking": "error",
1003
+ "dillon-anti-slop/no-module-mocking": mockingCustomized ? mockingRule([]) : "error",
969
1004
  "modules/test-modifiers": [
970
1005
  "error",
971
1006
  { additionalTestFunctions: options.tests?.additionalTestFunctions ?? [] }
@@ -998,7 +1033,10 @@ function presetTestsConfig(context) {
998
1033
  }
999
1034
  ]
1000
1035
  } : {}
1001
- })
1036
+ }),
1037
+ ...mockingAllow.map((entry) => presetOverride(entry.files, {
1038
+ "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis)
1039
+ }))
1002
1040
  ];
1003
1041
  return policies;
1004
1042
  }
@@ -1501,9 +1539,10 @@ function presetResolveContext(options) {
1501
1539
 
1502
1540
  // src/services/preset-compose.ts
1503
1541
  function presetCompose(context, policies, plugins) {
1504
- const { options, level, safetyFiles, ui, generated } = context;
1542
+ const { options, level, app, safetyFiles, ui, generated } = context;
1505
1543
  const severities = options.ruleSeverities ?? {};
1506
1544
  const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
1545
+ const maxLines = (max) => levels({ "max-lines": ["error", { max, skipBlankLines: true, skipComments: true }] });
1507
1546
  const overrideList = [];
1508
1547
  for (const name of presetPolicyOrder) {
1509
1548
  const setting = options.policies?.[name];
@@ -1520,9 +1559,9 @@ function presetCompose(context, policies, plugins) {
1520
1559
  }
1521
1560
  }
1522
1561
  overrideList.push(presetOverride(safetyFiles, levels({
1523
- "max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1562
+ ...maxLines(options.limits?.maxFileLines ?? 400),
1524
1563
  complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
1525
- }), ui));
1564
+ }), ui), presetOverride(presetScopes(app, "**/*.types.ts"), maxLines(options.limits?.maxTypeFileLines ?? 1000), ui));
1526
1565
  const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
1527
1566
  const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
1528
1567
  for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {