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 +9 -0
- package/README.md +5 -1
- package/dist/constants/preset.constants.d.ts +5 -1
- package/dist/index.js +5 -0
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +44 -5
- package/dist/presets/next-react-modules-preset.js +44 -5
- package/dist/presets/react-modules-preset.js +44 -5
- package/dist/presets/tanstack-start-react-modules-preset.js +44 -5
- package/dist/rules/dillon-anti-slop/index.js +1155 -24
- package/dist/rules/dillon-anti-slop/rules/no-module-mocking.d.ts +7 -0
- package/dist/rules/module-policy/index.js +5 -0
- package/dist/types/preset.types.d.ts +21 -0
- package/dist/utils/helpers/index.d.ts +1 -0
- package/dist/utils/helpers/mocking.d.ts +2 -0
- package/package.json +1 -1
- package/src/rules/dillon-anti-slop/UPSTREAM.md +6 -0
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;
|
package/dist/presets/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/presets/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? {})) {
|