oxlint-plugin-effect 0.10.0 → 0.12.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 +58 -22
- package/dist/presets/recommended.d.ts +20 -0
- package/dist/presets/recommended.js +15 -1
- package/dist/rules/_effect-context.js +27 -0
- package/dist/rules/_effect-namespaces.js +25 -0
- package/dist/rules/_function-metrics.js +283 -0
- package/dist/rules/_max-option.js +19 -0
- package/dist/rules/_tagged-control-flow.js +1 -1
- package/dist/rules/index.d.ts +14 -1
- package/dist/rules/index.js +27 -1
- package/dist/rules/max-cognitive-complexity.d.ts +6 -0
- package/dist/rules/max-cognitive-complexity.js +43 -0
- package/dist/rules/max-halstead-difficulty.d.ts +6 -0
- package/dist/rules/max-halstead-difficulty.js +44 -0
- package/dist/rules/no-inline-provide.d.ts +6 -0
- package/dist/rules/no-inline-provide.js +39 -0
- package/dist/rules/no-managed-runtime-in-effect.d.ts +6 -0
- package/dist/rules/no-managed-runtime-in-effect.js +41 -0
- package/dist/rules/no-nested-effect-gen.d.ts +6 -0
- package/dist/rules/no-nested-effect-gen.js +39 -0
- package/dist/rules/no-per-call-cache-construction.d.ts +6 -0
- package/dist/rules/no-per-call-cache-construction.js +52 -0
- package/dist/rules/no-run-collect-on-unbounded-stream.d.ts +6 -0
- package/dist/rules/no-run-collect-on-unbounded-stream.js +81 -0
- package/dist/rules/no-sequential-effect-all.d.ts +6 -0
- package/dist/rules/no-sequential-effect-all.js +61 -0
- package/dist/rules/no-silent-catch-all.d.ts +6 -0
- package/dist/rules/no-silent-catch-all.js +51 -0
- package/dist/rules/no-unbounded-concurrency.d.ts +6 -0
- package/dist/rules/no-unbounded-concurrency.js +50 -0
- package/dist/rules/no-unbounded-retry.d.ts +6 -0
- package/dist/rules/no-unbounded-retry.js +114 -0
- package/dist/rules/prefer-catch-tag.js +33 -6
- package/dist/rules/prefer-match-tags-exhaustive.js +62 -8
- package/dist/rules/prefer-service-of.d.ts +6 -0
- package/dist/rules/prefer-service-of.js +65 -0
- package/dist/rules/require-named-effect-fn.d.ts +6 -0
- package/dist/rules/require-named-effect-fn.js +37 -0
- package/dist/vendor/effect-oxlint/Rule.d.ts +11 -1
- package/dist/vendor/effect-oxlint/Rule.js +3 -0
- package/package.json +1 -1
package/dist/rules/index.js
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
import { __exportAll } from "../_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { noEffectBind } from "./no-effect-bind.js";
|
|
3
3
|
import { noEffectDo } from "./no-effect-do.js";
|
|
4
|
+
import { maxCognitiveComplexity } from "./max-cognitive-complexity.js";
|
|
5
|
+
import { maxHalsteadDifficulty } from "./max-halstead-difficulty.js";
|
|
4
6
|
import { noAs } from "./no-as.js";
|
|
5
7
|
import { noAsyncFunction } from "./no-async-function.js";
|
|
6
8
|
import { noChainedTypeAssertions } from "./no-chained-type-assertions.js";
|
|
7
9
|
import { noConditionalEmptyObjectSpread } from "./no-conditional-empty-object-spread.js";
|
|
8
10
|
import { noDynamicImports } from "./no-dynamic-imports.js";
|
|
9
11
|
import { noGlobals } from "./no-globals.js";
|
|
12
|
+
import { noInlineProvide } from "./no-inline-provide.js";
|
|
10
13
|
import { noKnownValueWidening } from "./no-known-value-widening.js";
|
|
14
|
+
import { noManagedRuntimeInEffect } from "./no-managed-runtime-in-effect.js";
|
|
11
15
|
import { noModuleMocks } from "./no-module-mocks.js";
|
|
16
|
+
import { noNestedEffectGen } from "./no-nested-effect-gen.js";
|
|
12
17
|
import { noNewError } from "./no-new-error.js";
|
|
13
18
|
import { noNewPromise } from "./no-new-promise.js";
|
|
14
19
|
import { noNodeBuiltinImport } from "./no-node-builtin-import.js";
|
|
15
20
|
import { noNullish } from "./no-nullish.js";
|
|
16
21
|
import { noObjectParameters } from "./no-object-parameters.js";
|
|
22
|
+
import { noPerCallCacheConstruction } from "./no-per-call-cache-construction.js";
|
|
23
|
+
import { noRunCollectOnUnboundedStream } from "./no-run-collect-on-unbounded-stream.js";
|
|
17
24
|
import { noRuntimeTypeof } from "./no-runtime-typeof.js";
|
|
25
|
+
import { noSequentialEffectAll } from "./no-sequential-effect-all.js";
|
|
18
26
|
import { noShapeInSymbolNames } from "./no-shape-in-symbol-names.js";
|
|
27
|
+
import { noSilentCatchAll } from "./no-silent-catch-all.js";
|
|
19
28
|
import { noTernary } from "./no-ternary.js";
|
|
20
29
|
import { noTestLifecycleHooks } from "./no-test-lifecycle-hooks.js";
|
|
21
30
|
import { noThrowStatement } from "./no-throw-statement.js";
|
|
22
31
|
import { noTryCatch } from "./no-try-catch.js";
|
|
32
|
+
import { noUnboundedConcurrency } from "./no-unbounded-concurrency.js";
|
|
33
|
+
import { noUnboundedRetry } from "./no-unbounded-retry.js";
|
|
23
34
|
import { noUnknownParameters } from "./no-unknown-parameters.js";
|
|
24
35
|
import { noUnknownTypeAliases } from "./no-unknown-type-aliases.js";
|
|
25
36
|
import { noUnsafeDictionaryType } from "./no-unsafe-dictionary-type.js";
|
|
@@ -28,8 +39,12 @@ import { preferCatchTag } from "./prefer-catch-tag.js";
|
|
|
28
39
|
import { preferEffectFn } from "./prefer-effect-fn.js";
|
|
29
40
|
import { preferMatchTagsExhaustive } from "./prefer-match-tags-exhaustive.js";
|
|
30
41
|
import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
|
|
42
|
+
import { preferServiceOf } from "./prefer-service-of.js";
|
|
43
|
+
import { requireNamedEffectFn } from "./require-named-effect-fn.js";
|
|
31
44
|
//#region src/rules/index.ts
|
|
32
45
|
var rules_exports = /* @__PURE__ */ __exportAll({
|
|
46
|
+
maxCognitiveComplexity: () => maxCognitiveComplexity,
|
|
47
|
+
maxHalsteadDifficulty: () => maxHalsteadDifficulty,
|
|
33
48
|
noAs: () => noAs,
|
|
34
49
|
noAsyncFunction: () => noAsyncFunction,
|
|
35
50
|
noChainedTypeAssertions: () => noChainedTypeAssertions,
|
|
@@ -38,19 +53,28 @@ var rules_exports = /* @__PURE__ */ __exportAll({
|
|
|
38
53
|
noEffectBind: () => noEffectBind,
|
|
39
54
|
noEffectDo: () => noEffectDo,
|
|
40
55
|
noGlobals: () => noGlobals,
|
|
56
|
+
noInlineProvide: () => noInlineProvide,
|
|
41
57
|
noKnownValueWidening: () => noKnownValueWidening,
|
|
58
|
+
noManagedRuntimeInEffect: () => noManagedRuntimeInEffect,
|
|
42
59
|
noModuleMocks: () => noModuleMocks,
|
|
60
|
+
noNestedEffectGen: () => noNestedEffectGen,
|
|
43
61
|
noNewError: () => noNewError,
|
|
44
62
|
noNewPromise: () => noNewPromise,
|
|
45
63
|
noNodeBuiltinImport: () => noNodeBuiltinImport,
|
|
46
64
|
noNullish: () => noNullish,
|
|
47
65
|
noObjectParameters: () => noObjectParameters,
|
|
66
|
+
noPerCallCacheConstruction: () => noPerCallCacheConstruction,
|
|
67
|
+
noRunCollectOnUnboundedStream: () => noRunCollectOnUnboundedStream,
|
|
48
68
|
noRuntimeTypeof: () => noRuntimeTypeof,
|
|
69
|
+
noSequentialEffectAll: () => noSequentialEffectAll,
|
|
49
70
|
noShapeInSymbolNames: () => noShapeInSymbolNames,
|
|
71
|
+
noSilentCatchAll: () => noSilentCatchAll,
|
|
50
72
|
noTernary: () => noTernary,
|
|
51
73
|
noTestLifecycleHooks: () => noTestLifecycleHooks,
|
|
52
74
|
noThrowStatement: () => noThrowStatement,
|
|
53
75
|
noTryCatch: () => noTryCatch,
|
|
76
|
+
noUnboundedConcurrency: () => noUnboundedConcurrency,
|
|
77
|
+
noUnboundedRetry: () => noUnboundedRetry,
|
|
54
78
|
noUnknownParameters: () => noUnknownParameters,
|
|
55
79
|
noUnknownTypeAliases: () => noUnknownTypeAliases,
|
|
56
80
|
noUnsafeDictionaryType: () => noUnsafeDictionaryType,
|
|
@@ -58,7 +82,9 @@ var rules_exports = /* @__PURE__ */ __exportAll({
|
|
|
58
82
|
preferCatchTag: () => preferCatchTag,
|
|
59
83
|
preferEffectFn: () => preferEffectFn,
|
|
60
84
|
preferMatchTagsExhaustive: () => preferMatchTagsExhaustive,
|
|
61
|
-
preferPredicateIsTagged: () => preferPredicateIsTagged
|
|
85
|
+
preferPredicateIsTagged: () => preferPredicateIsTagged,
|
|
86
|
+
preferServiceOf: () => preferServiceOf,
|
|
87
|
+
requireNamedEffectFn: () => requireNamedEffectFn
|
|
62
88
|
});
|
|
63
89
|
//#endregion
|
|
64
90
|
export { rules_exports };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { cognitiveComplexity, isFunctionNode } from "./_function-metrics.js";
|
|
6
|
+
import { MaxOptions, maxOptionsMeta, resolveMax } from "./_max-option.js";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/max-cognitive-complexity.ts
|
|
9
|
+
const DEFAULT_MAX = 21;
|
|
10
|
+
const maxCognitiveComplexity = define({
|
|
11
|
+
name: "max-cognitive-complexity",
|
|
12
|
+
meta: meta({
|
|
13
|
+
type: "suggestion",
|
|
14
|
+
description: "Keep every function below the cognitive complexity limit.",
|
|
15
|
+
messages: { tooComplex: "Function has a cognitive complexity of {{value}}. Maximum allowed is {{max}}. Split it into named operations." },
|
|
16
|
+
...maxOptionsMeta(DEFAULT_MAX)
|
|
17
|
+
}),
|
|
18
|
+
options: MaxOptions,
|
|
19
|
+
create: function* (options) {
|
|
20
|
+
const context = yield* RuleContext;
|
|
21
|
+
const max = resolveMax(options, DEFAULT_MAX);
|
|
22
|
+
const check = (node) => {
|
|
23
|
+
if (!isFunctionNode(node)) return Effect.void;
|
|
24
|
+
const value = cognitiveComplexity(node);
|
|
25
|
+
if (value <= max) return Effect.void;
|
|
26
|
+
return context.report(fromId({
|
|
27
|
+
node,
|
|
28
|
+
messageId: "tooComplex",
|
|
29
|
+
data: {
|
|
30
|
+
value: String(value),
|
|
31
|
+
max: String(max)
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
ArrowFunctionExpression: check,
|
|
37
|
+
FunctionDeclaration: check,
|
|
38
|
+
FunctionExpression: check
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
//#endregion
|
|
43
|
+
export { maxCognitiveComplexity };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { halstead, halsteadDifficulty, isFunctionNode } from "./_function-metrics.js";
|
|
6
|
+
import { MaxOptions, maxOptionsMeta, resolveMax } from "./_max-option.js";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/max-halstead-difficulty.ts
|
|
9
|
+
const DEFAULT_MAX = 79;
|
|
10
|
+
const formatDifficulty = (value) => String(Math.round(value * 10) / 10);
|
|
11
|
+
const maxHalsteadDifficulty = define({
|
|
12
|
+
name: "max-halstead-difficulty",
|
|
13
|
+
meta: meta({
|
|
14
|
+
type: "suggestion",
|
|
15
|
+
description: "Keep every function below the Halstead difficulty limit.",
|
|
16
|
+
messages: { tooDifficult: "Function has a Halstead difficulty of {{value}}. Maximum allowed is {{max}}. Extract repeated expressions into named values." },
|
|
17
|
+
...maxOptionsMeta(DEFAULT_MAX)
|
|
18
|
+
}),
|
|
19
|
+
options: MaxOptions,
|
|
20
|
+
create: function* (options) {
|
|
21
|
+
const context = yield* RuleContext;
|
|
22
|
+
const max = resolveMax(options, DEFAULT_MAX);
|
|
23
|
+
const check = (node) => {
|
|
24
|
+
if (!isFunctionNode(node)) return Effect.void;
|
|
25
|
+
const value = halsteadDifficulty(halstead(node));
|
|
26
|
+
if (value <= max) return Effect.void;
|
|
27
|
+
return context.report(fromId({
|
|
28
|
+
node,
|
|
29
|
+
messageId: "tooDifficult",
|
|
30
|
+
data: {
|
|
31
|
+
value: formatDifficulty(value),
|
|
32
|
+
max: String(max)
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
ArrowFunctionExpression: check,
|
|
38
|
+
FunctionDeclaration: check,
|
|
39
|
+
FunctionExpression: check
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
//#endregion
|
|
44
|
+
export { maxHalsteadDifficulty };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
|
+
import { isInsideEffectProgram } from "./_effect-context.js";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/no-inline-provide.ts
|
|
9
|
+
/** Keep Effect provisioning at explicit composition boundaries. */
|
|
10
|
+
const noInlineProvide = define({
|
|
11
|
+
name: "no-inline-provide",
|
|
12
|
+
meta: meta({
|
|
13
|
+
type: "problem",
|
|
14
|
+
description: "Keep Effect provisioning outside domain Effect operations."
|
|
15
|
+
}),
|
|
16
|
+
create: function* () {
|
|
17
|
+
const ctx = yield* RuleContext;
|
|
18
|
+
const effectNamespaces = new Set(["Effect"]);
|
|
19
|
+
return {
|
|
20
|
+
ImportDeclaration: (node) => {
|
|
21
|
+
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
22
|
+
for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
|
|
23
|
+
return Effect.void;
|
|
24
|
+
},
|
|
25
|
+
CallExpression: (node) => {
|
|
26
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
27
|
+
const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
|
|
28
|
+
if (!isStaticMember(node.callee, namespaces, "provide")) return Effect.void;
|
|
29
|
+
if (!isInsideEffectProgram(node, namespaces)) return Effect.void;
|
|
30
|
+
return ctx.report(make({
|
|
31
|
+
node,
|
|
32
|
+
message: "Do not provide dependencies inside an Effect operation. Provide the layer once at the application, handler, command, or test boundary."
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
//#endregion
|
|
39
|
+
export { noInlineProvide };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
|
+
import { isInsideEffectProgram } from "./_effect-context.js";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/no-managed-runtime-in-effect.ts
|
|
9
|
+
/** Keep ManagedRuntime construction at non-Effect host boundaries. */
|
|
10
|
+
const noManagedRuntimeInEffect = define({
|
|
11
|
+
name: "no-managed-runtime-in-effect",
|
|
12
|
+
meta: meta({
|
|
13
|
+
type: "problem",
|
|
14
|
+
description: "Create ManagedRuntime only at a non-Effect host boundary."
|
|
15
|
+
}),
|
|
16
|
+
create: function* () {
|
|
17
|
+
const ctx = yield* RuleContext;
|
|
18
|
+
const effectNamespaces = new Set(["Effect"]);
|
|
19
|
+
const managedRuntimeNamespaces = new Set(["ManagedRuntime"]);
|
|
20
|
+
return {
|
|
21
|
+
ImportDeclaration: (node) => {
|
|
22
|
+
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
23
|
+
for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
|
|
24
|
+
for (const name of importedNamespaces(node, "ManagedRuntime", "effect/ManagedRuntime")) managedRuntimeNamespaces.add(name);
|
|
25
|
+
return Effect.void;
|
|
26
|
+
},
|
|
27
|
+
CallExpression: (node) => {
|
|
28
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
29
|
+
const runtimes = visibleNamespaces(ctx, node, managedRuntimeNamespaces);
|
|
30
|
+
if (!isStaticMember(node.callee, runtimes, "make")) return Effect.void;
|
|
31
|
+
if (!isInsideEffectProgram(node, visibleNamespaces(ctx, node, effectNamespaces))) return Effect.void;
|
|
32
|
+
return ctx.report(make({
|
|
33
|
+
node,
|
|
34
|
+
message: "Create ManagedRuntime only at a non-Effect host boundary. Use the current Effect environment inside Effect programs."
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
//#endregion
|
|
41
|
+
export { noManagedRuntimeInEffect };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
|
+
import { isInsideEffectProgram } from "./_effect-context.js";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/no-nested-effect-gen.ts
|
|
9
|
+
const isDirectYield = (node) => node.parent?.type === "YieldExpression";
|
|
10
|
+
const noNestedEffectGen = define({
|
|
11
|
+
name: "no-nested-effect-gen",
|
|
12
|
+
meta: meta({
|
|
13
|
+
type: "suggestion",
|
|
14
|
+
description: "Flatten directly yielded nested Effect.gen programs."
|
|
15
|
+
}),
|
|
16
|
+
create: function* () {
|
|
17
|
+
const ctx = yield* RuleContext;
|
|
18
|
+
const effectNamespaces = new Set(["Effect"]);
|
|
19
|
+
return {
|
|
20
|
+
ImportDeclaration: (node) => {
|
|
21
|
+
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
22
|
+
for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
|
|
23
|
+
return Effect.void;
|
|
24
|
+
},
|
|
25
|
+
CallExpression: (node) => {
|
|
26
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
27
|
+
const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
|
|
28
|
+
if (!isStaticMember(node.callee, namespaces, "gen") || !isDirectYield(node)) return Effect.void;
|
|
29
|
+
if (!isInsideEffectProgram(node, namespaces)) return Effect.void;
|
|
30
|
+
return ctx.report(make({
|
|
31
|
+
node,
|
|
32
|
+
message: "Flatten this directly yielded Effect.gen into the owning Effect operation."
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
//#endregion
|
|
39
|
+
export { noNestedEffectGen };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
|
+
import { enclosingEffectProgram } from "./_effect-context.js";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/no-per-call-cache-construction.ts
|
|
9
|
+
const cachedEffectOperations = new Set([
|
|
10
|
+
"cached",
|
|
11
|
+
"cachedInvalidateWithTTL",
|
|
12
|
+
"cachedWithTTL"
|
|
13
|
+
]);
|
|
14
|
+
const cacheConstructors = new Set(["make", "makeWith"]);
|
|
15
|
+
const isOperation = (node, namespaces, operations) => {
|
|
16
|
+
for (const operation of operations) if (isStaticMember(node, namespaces, operation)) return true;
|
|
17
|
+
return false;
|
|
18
|
+
};
|
|
19
|
+
const noPerCallCacheConstruction = define({
|
|
20
|
+
name: "no-per-call-cache-construction",
|
|
21
|
+
meta: meta({
|
|
22
|
+
type: "suggestion",
|
|
23
|
+
description: "Construct shared caches once in their owning layer."
|
|
24
|
+
}),
|
|
25
|
+
create: function* () {
|
|
26
|
+
const ctx = yield* RuleContext;
|
|
27
|
+
const effectNamespaces = new Set(["Effect"]);
|
|
28
|
+
const cacheNamespaces = new Set(["Cache"]);
|
|
29
|
+
return {
|
|
30
|
+
ImportDeclaration: (node) => {
|
|
31
|
+
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
32
|
+
for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
|
|
33
|
+
for (const name of importedNamespaces(node, "Cache", "effect/Cache")) cacheNamespaces.add(name);
|
|
34
|
+
return Effect.void;
|
|
35
|
+
},
|
|
36
|
+
CallExpression: (node) => {
|
|
37
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
38
|
+
const effects = visibleNamespaces(ctx, node, effectNamespaces);
|
|
39
|
+
const program = enclosingEffectProgram(node, effects);
|
|
40
|
+
if (program === void 0 || program[1] === "gen") return Effect.void;
|
|
41
|
+
const caches = visibleNamespaces(ctx, node, cacheNamespaces);
|
|
42
|
+
if (!(isOperation(node.callee, effects, cachedEffectOperations) || isOperation(node.callee, caches, cacheConstructors))) return Effect.void;
|
|
43
|
+
return ctx.report(make({
|
|
44
|
+
node,
|
|
45
|
+
message: "Construct this cache once in its owning layer. Creating it inside an Effect operation creates new cache state on every call."
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
//#endregion
|
|
52
|
+
export { noPerCallCacheConstruction };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/rules/no-run-collect-on-unbounded-stream.ts
|
|
8
|
+
const unboundedSources = new Set([
|
|
9
|
+
"fromPubSub",
|
|
10
|
+
"fromPubSubTake",
|
|
11
|
+
"fromQueue",
|
|
12
|
+
"repeat"
|
|
13
|
+
]);
|
|
14
|
+
const collectOperations = new Set(["runCollect"]);
|
|
15
|
+
const terminatingOperations = new Set([
|
|
16
|
+
"take",
|
|
17
|
+
"takeUntil",
|
|
18
|
+
"takeUntilEffect",
|
|
19
|
+
"takeWhile",
|
|
20
|
+
"takeWhileEffect",
|
|
21
|
+
"timeout",
|
|
22
|
+
"timeoutOrElse"
|
|
23
|
+
]);
|
|
24
|
+
const staticOperation = (node, streamNamespaces, operations) => {
|
|
25
|
+
if (node.type === "SpreadElement") return false;
|
|
26
|
+
const candidate = node.type === "CallExpression" && node.callee.type !== "Super" ? node.callee : node;
|
|
27
|
+
for (const operation of operations) if (isStaticMember(candidate, streamNamespaces, operation)) return true;
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
const unboundedRoot = (node, streamNamespaces) => {
|
|
31
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return void 0;
|
|
32
|
+
for (const source of unboundedSources) if (isStaticMember(node.callee, streamNamespaces, source)) return node;
|
|
33
|
+
if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "pipe") return;
|
|
34
|
+
const root = unboundedRoot(node.callee.object, streamNamespaces);
|
|
35
|
+
if (root === void 0) return void 0;
|
|
36
|
+
for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, terminatingOperations)) return void 0;
|
|
37
|
+
return root;
|
|
38
|
+
};
|
|
39
|
+
const collectedSource = (node, streamNamespaces) => {
|
|
40
|
+
if (node.callee.type === "Super") return void 0;
|
|
41
|
+
if (isStaticMember(node.callee, streamNamespaces, "runCollect")) {
|
|
42
|
+
const source = node.arguments[0];
|
|
43
|
+
if (source !== void 0 && source.type !== "SpreadElement") return source;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "pipe") return;
|
|
47
|
+
for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, terminatingOperations)) return void 0;
|
|
48
|
+
for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, collectOperations)) return node.callee.object;
|
|
49
|
+
};
|
|
50
|
+
const noRunCollectOnUnboundedStream = define({
|
|
51
|
+
name: "no-run-collect-on-unbounded-stream",
|
|
52
|
+
meta: meta({
|
|
53
|
+
type: "problem",
|
|
54
|
+
description: "Do not collect a clearly unbounded Stream without a terminating operation."
|
|
55
|
+
}),
|
|
56
|
+
create: function* () {
|
|
57
|
+
const ctx = yield* RuleContext;
|
|
58
|
+
const streamNamespaces = new Set(["Stream"]);
|
|
59
|
+
return {
|
|
60
|
+
ImportDeclaration: (node) => {
|
|
61
|
+
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
62
|
+
for (const name of importedNamespaces(node, "Stream", "effect/Stream")) streamNamespaces.add(name);
|
|
63
|
+
return Effect.void;
|
|
64
|
+
},
|
|
65
|
+
CallExpression: (node) => {
|
|
66
|
+
if (node.type !== "CallExpression") return Effect.void;
|
|
67
|
+
const namespaces = visibleNamespaces(ctx, node, streamNamespaces);
|
|
68
|
+
const source = collectedSource(node, namespaces);
|
|
69
|
+
if (source === void 0) return Effect.void;
|
|
70
|
+
const root = unboundedRoot(source, namespaces);
|
|
71
|
+
if (root === void 0) return Effect.void;
|
|
72
|
+
return ctx.report(make({
|
|
73
|
+
node: root,
|
|
74
|
+
message: "Do not collect a clearly unbounded Stream. Add a terminating operation or consume it with runForEach or runDrain."
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
//#endregion
|
|
81
|
+
export { noRunCollectOnUnboundedStream };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/rules/no-sequential-effect-all.ts
|
|
8
|
+
const propertyLiteral = (node, name) => {
|
|
9
|
+
if (node?.type !== "ObjectExpression") return void 0;
|
|
10
|
+
for (const property of node.properties) {
|
|
11
|
+
if (property.type !== "Property" || property.computed) continue;
|
|
12
|
+
if ((property.key.type === "Identifier" && property.key.name === name || property.key.type === "Literal" && property.key.value === name) && property.value.type === "Literal") {
|
|
13
|
+
const value = property.value.value;
|
|
14
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const pipedToAsVoid = (node, effectNamespaces) => {
|
|
19
|
+
const member = node.parent;
|
|
20
|
+
if (member?.type !== "MemberExpression" || member.object !== node || member.computed || member.property.type !== "Identifier" || member.property.name !== "pipe") return false;
|
|
21
|
+
const pipe = member.parent;
|
|
22
|
+
if (pipe?.type !== "CallExpression") return false;
|
|
23
|
+
for (const operation of pipe.arguments) {
|
|
24
|
+
if (operation.type === "SpreadElement") continue;
|
|
25
|
+
if (isStaticMember(operation, effectNamespaces, "asVoid")) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
const noSequentialEffectAll = define({
|
|
30
|
+
name: "no-sequential-effect-all",
|
|
31
|
+
meta: meta({
|
|
32
|
+
type: "suggestion",
|
|
33
|
+
description: "Use explicit sequencing instead of a serial Effect.all whose result is discarded."
|
|
34
|
+
}),
|
|
35
|
+
create: function* () {
|
|
36
|
+
const ctx = yield* RuleContext;
|
|
37
|
+
const effectNamespaces = new Set(["Effect"]);
|
|
38
|
+
return {
|
|
39
|
+
ImportDeclaration: (node) => {
|
|
40
|
+
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
41
|
+
for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
|
|
42
|
+
return Effect.void;
|
|
43
|
+
},
|
|
44
|
+
CallExpression: (node) => {
|
|
45
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
46
|
+
const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
|
|
47
|
+
if (!isStaticMember(node.callee, namespaces, "all")) return Effect.void;
|
|
48
|
+
if (node.arguments[0]?.type !== "ArrayExpression") return Effect.void;
|
|
49
|
+
const options = node.arguments[1];
|
|
50
|
+
if (propertyLiteral(options, "concurrency") !== 1) return Effect.void;
|
|
51
|
+
if (!(propertyLiteral(options, "discard") === true || pipedToAsVoid(node, namespaces))) return Effect.void;
|
|
52
|
+
return ctx.report(make({
|
|
53
|
+
node,
|
|
54
|
+
message: "Use Effect.andThen or one flat generator for sequential steps. Reserve Effect.all for value aggregation or real concurrency."
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
//#endregion
|
|
61
|
+
export { noSequentialEffectAll };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/rules/no-silent-catch-all.ts
|
|
8
|
+
const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
9
|
+
const isEffectVoid = (node, effectNamespaces) => isStaticMember(node, effectNamespaces, "void");
|
|
10
|
+
const silentlyReturnsVoid = (node, effectNamespaces) => {
|
|
11
|
+
if (node.params.length !== 0 || node.body === null) return false;
|
|
12
|
+
if (node.body.type !== "BlockStatement") return isEffectVoid(node.body, effectNamespaces);
|
|
13
|
+
if (node.body.body.length !== 1) return false;
|
|
14
|
+
const statement = node.body.body[0];
|
|
15
|
+
return statement?.type === "ReturnStatement" && statement.argument !== null && isEffectVoid(statement.argument, effectNamespaces);
|
|
16
|
+
};
|
|
17
|
+
const handlerArgument = (node) => {
|
|
18
|
+
if (node.arguments.length === 1) return node.arguments[0];
|
|
19
|
+
return node.arguments[1];
|
|
20
|
+
};
|
|
21
|
+
const noSilentCatchAll = define({
|
|
22
|
+
name: "no-silent-catch-all",
|
|
23
|
+
meta: meta({
|
|
24
|
+
type: "problem",
|
|
25
|
+
description: "Do not silently erase every failure with Effect.void."
|
|
26
|
+
}),
|
|
27
|
+
create: function* () {
|
|
28
|
+
const ctx = yield* RuleContext;
|
|
29
|
+
const effectNamespaces = new Set(["Effect"]);
|
|
30
|
+
return {
|
|
31
|
+
ImportDeclaration: (node) => {
|
|
32
|
+
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
33
|
+
for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
|
|
34
|
+
return Effect.void;
|
|
35
|
+
},
|
|
36
|
+
CallExpression: (node) => {
|
|
37
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
38
|
+
const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
|
|
39
|
+
if (!(isStaticMember(node.callee, namespaces, "catchAll") || isStaticMember(node.callee, namespaces, "catchAllCause"))) return Effect.void;
|
|
40
|
+
const handler = handlerArgument(node);
|
|
41
|
+
if (!isFunction(handler) || !silentlyReturnsVoid(handler, namespaces)) return Effect.void;
|
|
42
|
+
return ctx.report(make({
|
|
43
|
+
node: handler,
|
|
44
|
+
message: "Do not erase every failure with Effect.void. Recover a typed failure truthfully or record the unexpected failure before recovery."
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
//#endregion
|
|
51
|
+
export { noSilentCatchAll };
|