oxlint-plugin-effect 0.9.0 → 0.10.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/README.md
CHANGED
|
@@ -35,6 +35,7 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
|
|
|
35
35
|
| `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
|
|
36
36
|
| `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
|
|
37
37
|
| `effect/noNullish` | Bans null and undefined; use Option or a domain enum for richer state |
|
|
38
|
+
| `effect/noModuleMocks` | Bans Vitest and Jest module mocks and method spies; use Effect service test layers |
|
|
38
39
|
| `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
|
|
39
40
|
| `effect/noDynamicImports` | Allows import() only behind a named lazy-loading boundary; bans require() |
|
|
40
41
|
| `effect/noEffectDo` | Bans Effect.Do |
|
|
@@ -14,6 +14,7 @@ declare const recommended: {
|
|
|
14
14
|
readonly "effect/noEffectDo": "error";
|
|
15
15
|
readonly "effect/noGlobals": "error";
|
|
16
16
|
readonly "effect/noKnownValueWidening": "error";
|
|
17
|
+
readonly "effect/noModuleMocks": "error";
|
|
17
18
|
readonly "effect/noNewError": "error";
|
|
18
19
|
readonly "effect/noNewPromise": "error";
|
|
19
20
|
readonly "effect/noNodeBuiltinImport": "error";
|
|
@@ -14,6 +14,7 @@ const recommended = {
|
|
|
14
14
|
"effect/noEffectDo": "error",
|
|
15
15
|
"effect/noGlobals": "error",
|
|
16
16
|
"effect/noKnownValueWidening": "error",
|
|
17
|
+
"effect/noModuleMocks": "error",
|
|
17
18
|
"effect/noNewError": "error",
|
|
18
19
|
"effect/noNewPromise": "error",
|
|
19
20
|
"effect/noNodeBuiltinImport": "error",
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { noConditionalEmptyObjectSpread } from "./no-conditional-empty-object-sp
|
|
|
7
7
|
import { noDynamicImports } from "./no-dynamic-imports.js";
|
|
8
8
|
import { noGlobals } from "./no-globals.js";
|
|
9
9
|
import { noKnownValueWidening } from "./no-known-value-widening.js";
|
|
10
|
+
import { noModuleMocks } from "./no-module-mocks.js";
|
|
10
11
|
import { noNewError } from "./no-new-error.js";
|
|
11
12
|
import { noNewPromise } from "./no-new-promise.js";
|
|
12
13
|
import { noNodeBuiltinImport } from "./no-node-builtin-import.js";
|
|
@@ -29,7 +30,7 @@ import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
|
|
|
29
30
|
|
|
30
31
|
//#region src/rules/index.d.ts
|
|
31
32
|
declare namespace index_d_exports {
|
|
32
|
-
export { noAs, noAsyncFunction, noChainedTypeAssertions, noConditionalEmptyObjectSpread, noDynamicImports, noEffectBind, noEffectDo, noGlobals, noKnownValueWidening, noNewError, noNewPromise, noNodeBuiltinImport, noNullish, noObjectParameters, noRuntimeTypeof, noShapeInSymbolNames, noTernary, noTestLifecycleHooks, noThrowStatement, noTryCatch, noUnknownParameters, noUnknownTypeAliases, noUnsafeDictionaryType, noWidenThenAssert, preferCatchTag, preferEffectFn, preferMatchTagsExhaustive, preferPredicateIsTagged };
|
|
33
|
+
export { noAs, noAsyncFunction, noChainedTypeAssertions, noConditionalEmptyObjectSpread, noDynamicImports, noEffectBind, noEffectDo, noGlobals, noKnownValueWidening, noModuleMocks, noNewError, noNewPromise, noNodeBuiltinImport, noNullish, noObjectParameters, noRuntimeTypeof, noShapeInSymbolNames, noTernary, noTestLifecycleHooks, noThrowStatement, noTryCatch, noUnknownParameters, noUnknownTypeAliases, noUnsafeDictionaryType, noWidenThenAssert, preferCatchTag, preferEffectFn, preferMatchTagsExhaustive, preferPredicateIsTagged };
|
|
33
34
|
}
|
|
34
35
|
//#endregion
|
|
35
36
|
export { index_d_exports };
|
package/dist/rules/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { noConditionalEmptyObjectSpread } from "./no-conditional-empty-object-sp
|
|
|
8
8
|
import { noDynamicImports } from "./no-dynamic-imports.js";
|
|
9
9
|
import { noGlobals } from "./no-globals.js";
|
|
10
10
|
import { noKnownValueWidening } from "./no-known-value-widening.js";
|
|
11
|
+
import { noModuleMocks } from "./no-module-mocks.js";
|
|
11
12
|
import { noNewError } from "./no-new-error.js";
|
|
12
13
|
import { noNewPromise } from "./no-new-promise.js";
|
|
13
14
|
import { noNodeBuiltinImport } from "./no-node-builtin-import.js";
|
|
@@ -38,6 +39,7 @@ var rules_exports = /* @__PURE__ */ __exportAll({
|
|
|
38
39
|
noEffectDo: () => noEffectDo,
|
|
39
40
|
noGlobals: () => noGlobals,
|
|
40
41
|
noKnownValueWidening: () => noKnownValueWidening,
|
|
42
|
+
noModuleMocks: () => noModuleMocks,
|
|
41
43
|
noNewError: () => noNewError,
|
|
42
44
|
noNewPromise: () => noNewPromise,
|
|
43
45
|
noNodeBuiltinImport: () => noNodeBuiltinImport,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { importSource, memberNames, narrow } from "../vendor/effect-oxlint/AST.js";
|
|
2
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
3
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
4
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
5
|
+
import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
|
|
6
|
+
import "../vendor/effect-oxlint/index.js";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
//#region src/rules/no-module-mocks.ts
|
|
10
|
+
const bannedMethods = new Set(["mock", "spyOn"]);
|
|
11
|
+
const identifierName = (node) => {
|
|
12
|
+
if (node?.type === "Identifier") return node.name;
|
|
13
|
+
};
|
|
14
|
+
const globalTestApi = (name) => {
|
|
15
|
+
if (name === "jest" || name === "vi") return name;
|
|
16
|
+
};
|
|
17
|
+
const importedTestApi = (source, imported) => {
|
|
18
|
+
if (source === "vitest" && imported === "vi") return "vi";
|
|
19
|
+
if (source === "@jest/globals" && imported === "jest") return "jest";
|
|
20
|
+
};
|
|
21
|
+
const testApiFromVariable = (variable, importedBindings) => {
|
|
22
|
+
if (variable.defs.some((definition) => definition.type === "ImportBinding")) return importedBindings.get(variable.name);
|
|
23
|
+
};
|
|
24
|
+
const noModuleMocks = define({
|
|
25
|
+
name: "no-module-mocks",
|
|
26
|
+
meta: meta({
|
|
27
|
+
type: "problem",
|
|
28
|
+
description: "Use Effect service test layers instead of module mocks or method spies."
|
|
29
|
+
}),
|
|
30
|
+
create: function* () {
|
|
31
|
+
const context = yield* RuleContext;
|
|
32
|
+
const importedBindings = /* @__PURE__ */ new Map();
|
|
33
|
+
return {
|
|
34
|
+
ImportDeclaration: (node) => Option.match(narrow(node, "ImportDeclaration"), {
|
|
35
|
+
onNone: () => Effect.void,
|
|
36
|
+
onSome: (declaration) => {
|
|
37
|
+
const source = importSource(declaration);
|
|
38
|
+
for (const specifier of declaration.specifiers) {
|
|
39
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
40
|
+
const imported = identifierName(specifier.imported);
|
|
41
|
+
if (imported === void 0) continue;
|
|
42
|
+
const api = importedTestApi(source, imported);
|
|
43
|
+
if (api !== void 0) importedBindings.set(specifier.local.name, api);
|
|
44
|
+
}
|
|
45
|
+
return Effect.void;
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
CallExpression: (node) => Option.match(narrow(node, "CallExpression"), {
|
|
49
|
+
onNone: () => Effect.void,
|
|
50
|
+
onSome: (call) => {
|
|
51
|
+
if (call.callee.type !== "MemberExpression") return Effect.void;
|
|
52
|
+
const names = Option.getOrUndefined(memberNames(call.callee));
|
|
53
|
+
if (names === void 0) return Effect.void;
|
|
54
|
+
const [object, method] = names;
|
|
55
|
+
if (!bannedMethods.has(method)) return Effect.void;
|
|
56
|
+
const variable = Option.getOrUndefined(findVariableUp(context.sourceCode.getScope(call), object));
|
|
57
|
+
let api = globalTestApi(object);
|
|
58
|
+
if (variable !== void 0) {
|
|
59
|
+
api = testApiFromVariable(variable, importedBindings);
|
|
60
|
+
if (api === void 0 && variable.defs.length === 0) api = globalTestApi(object);
|
|
61
|
+
}
|
|
62
|
+
if (api === void 0) return Effect.void;
|
|
63
|
+
return context.report(make({
|
|
64
|
+
node: call,
|
|
65
|
+
message: `Avoid ${api}.${method}(). Replace the external boundary with an Effect service test Layer.`
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
//#endregion
|
|
73
|
+
export { noModuleMocks };
|