oxlint-plugin-effect 0.16.1 → 0.17.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.
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
|
|
2
|
+
import "../vendor/effect-oxlint/index.js";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
//#region src/rules/_const-bindings.ts
|
|
5
|
+
/** Strips type-only wrappers such as `as const`, `satisfies`, and `!`. */
|
|
6
|
+
const unwrapTypeSyntax = (node) => {
|
|
7
|
+
let current = node;
|
|
8
|
+
while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression") current = current.expression;
|
|
9
|
+
return current;
|
|
10
|
+
};
|
|
11
|
+
/** The initializer of a single, never reassigned const binding. */
|
|
12
|
+
const constInitializer = (ctx, node) => Option.flatMap(findVariableUp(ctx.sourceCode.getScope(node), node.name), (variable) => {
|
|
13
|
+
const definition = variable.defs[0];
|
|
14
|
+
if (variable.defs.length !== 1 || definition?.node.type !== "VariableDeclarator") return Option.none();
|
|
15
|
+
const declarator = definition.node;
|
|
16
|
+
if (declarator.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const") return Option.none();
|
|
17
|
+
return Option.fromNullishOr(declarator.init);
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* A value held in a const is checked like the inline value, through type-only wrappers.
|
|
21
|
+
* Unresolved identifiers, such as parameters, stay as they are. `seen` stops a self-referential chain.
|
|
22
|
+
*/
|
|
23
|
+
const constResolver = (ctx) => {
|
|
24
|
+
const resolveWith = (seen) => (expression) => {
|
|
25
|
+
const node = unwrapTypeSyntax(expression);
|
|
26
|
+
if (node.type !== "Identifier" || seen.has(node)) return node;
|
|
27
|
+
return Option.match(constInitializer(ctx, node), {
|
|
28
|
+
onNone: () => node,
|
|
29
|
+
onSome: (init) => resolveWith(new Set([...seen, node]))(init)
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
return resolveWith(/* @__PURE__ */ new Set());
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { constResolver };
|
|
@@ -4,14 +4,19 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
5
|
import { expressionArgument, staticProperties } from "./_call-arguments.js";
|
|
6
6
|
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
7
|
+
import { constResolver } from "./_const-bindings.js";
|
|
7
8
|
import * as Arr from "effect/Array";
|
|
8
9
|
import * as Option from "effect/Option";
|
|
9
10
|
import * as Effect from "effect/Effect";
|
|
10
11
|
//#region src/rules/no-unbounded-concurrency.ts
|
|
11
|
-
/**
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/** Whether an options object, inline or held in a const, sets `concurrency: "unbounded"`. */
|
|
13
|
+
const isUnboundedConcurrency = (argument, resolve) => {
|
|
14
|
+
const options = resolve(argument);
|
|
15
|
+
if (options.type !== "ObjectExpression") return false;
|
|
16
|
+
return staticProperties(options, "concurrency").some((property) => {
|
|
17
|
+
const value = resolve(property.value);
|
|
18
|
+
return value.type === "Literal" && value.value === "unbounded";
|
|
19
|
+
});
|
|
15
20
|
};
|
|
16
21
|
const fixedCollection = (node) => node.type === "ArrayExpression" || node.type === "ObjectExpression";
|
|
17
22
|
/** Where `Effect.all` and `Effect.forEach` take their options. */
|
|
@@ -29,6 +34,7 @@ const noUnboundedConcurrency = define({
|
|
|
29
34
|
create: function* () {
|
|
30
35
|
const ctx = yield* RuleContext;
|
|
31
36
|
const effectNamespaces = new Set(["Effect"]);
|
|
37
|
+
const resolve = constResolver(ctx);
|
|
32
38
|
return {
|
|
33
39
|
ImportDeclaration: (node) => {
|
|
34
40
|
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
@@ -39,14 +45,11 @@ const noUnboundedConcurrency = define({
|
|
|
39
45
|
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
40
46
|
const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
|
|
41
47
|
if (Option.exists(Arr.head(node.arguments), fixedCollection)) return Effect.void;
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
message: "Bound concurrency for a collection that can grow. Use a finite concurrency value."
|
|
48
|
-
}))
|
|
49
|
-
});
|
|
48
|
+
if (!Option.exists(optionsIndex(node.callee, namespaces), (index) => Option.exists(expressionArgument(node, index), (options) => isUnboundedConcurrency(options, resolve)))) return Effect.void;
|
|
49
|
+
return ctx.report(make({
|
|
50
|
+
node,
|
|
51
|
+
message: "Bound concurrency for a collection that can grow. Use a finite concurrency value."
|
|
52
|
+
}));
|
|
50
53
|
}
|
|
51
54
|
};
|
|
52
55
|
}
|
|
@@ -4,6 +4,7 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
5
|
import { expressionArgument, staticProperties } from "./_call-arguments.js";
|
|
6
6
|
import { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
7
|
+
import { constResolver } from "./_const-bindings.js";
|
|
7
8
|
import * as Arr from "effect/Array";
|
|
8
9
|
import * as Option from "effect/Option";
|
|
9
10
|
import * as Effect from "effect/Effect";
|
|
@@ -18,21 +19,25 @@ const staticPropertyName = (node) => {
|
|
|
18
19
|
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return Option.none();
|
|
19
20
|
return Option.some(node.property.name);
|
|
20
21
|
};
|
|
21
|
-
const isBoundedOperation = (
|
|
22
|
+
const isBoundedOperation = (argument, scheduleNamespaces, resolve) => {
|
|
23
|
+
if (argument.type === "SpreadElement") return false;
|
|
24
|
+
const node = resolve(argument);
|
|
22
25
|
if (node.type !== "CallExpression" || node.callee.type === "Super") return false;
|
|
23
26
|
const callee = node.callee;
|
|
24
27
|
if (Option.exists(staticPropertyName(callee), (property) => boundedCombinators.has(property) && isStaticMember(callee, scheduleNamespaces, property))) return true;
|
|
25
28
|
if (!isStaticMember(callee, scheduleNamespaces, "both")) return false;
|
|
26
|
-
return Option.exists(expressionArgument(node, 0), (bound) => isStaticCall(bound, scheduleNamespaces, "recurs"));
|
|
29
|
+
return Option.exists(expressionArgument(node, 0), (bound) => isStaticCall(resolve(bound), scheduleNamespaces, "recurs"));
|
|
27
30
|
};
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (node.type
|
|
31
|
+
const isUnboundedSchedule = (expression, scheduleNamespaces, resolve) => {
|
|
32
|
+
const node = resolve(expression);
|
|
33
|
+
if (node.type === "MemberExpression" && isStaticMember(node, scheduleNamespaces, "forever")) return true;
|
|
34
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return false;
|
|
31
35
|
const callee = node.callee;
|
|
32
36
|
const property = staticPropertyName(callee);
|
|
33
|
-
if (Option.exists(property, (name) => unboundedConstructors.has(name) && isStaticMember(callee, scheduleNamespaces, name))) return
|
|
34
|
-
if (!Option.contains(property, "pipe") || callee.type !== "MemberExpression") return
|
|
35
|
-
|
|
37
|
+
if (Option.exists(property, (name) => unboundedConstructors.has(name) && isStaticMember(callee, scheduleNamespaces, name))) return true;
|
|
38
|
+
if (!Option.contains(property, "pipe") || callee.type !== "MemberExpression") return false;
|
|
39
|
+
if (callee.object.type === "Super") return false;
|
|
40
|
+
return isUnboundedSchedule(callee.object, scheduleNamespaces, resolve) && !node.arguments.some((operation) => isBoundedOperation(operation, scheduleNamespaces, resolve));
|
|
36
41
|
};
|
|
37
42
|
/** Arguments a data-last retry call takes; one more means the data-first form. */
|
|
38
43
|
const retryArity = (callee, effectNamespaces, streamNamespaces, httpClientNamespaces) => {
|
|
@@ -48,16 +53,17 @@ const policyArgument = (node, arity) => {
|
|
|
48
53
|
return Arr.get(node.arguments, policyIndex);
|
|
49
54
|
};
|
|
50
55
|
/** The schedule a policy argument retries on, unless it sets `times`. */
|
|
51
|
-
const policySchedule = (
|
|
52
|
-
if (
|
|
56
|
+
const policySchedule = (argument, resolve) => {
|
|
57
|
+
if (argument.type === "SpreadElement") return Option.none();
|
|
58
|
+
const policy = resolve(argument);
|
|
53
59
|
if (policy.type !== "ObjectExpression") return Option.some(policy);
|
|
54
60
|
if (Arr.isReadonlyArrayNonEmpty(staticProperties(policy, "times"))) return Option.none();
|
|
55
61
|
return Option.map(Arr.head(staticProperties(policy, "schedule")), (property) => property.value);
|
|
56
62
|
};
|
|
57
|
-
const retryPolicy = (node, effectNamespaces, streamNamespaces, httpClientNamespaces) => {
|
|
63
|
+
const retryPolicy = (node, effectNamespaces, streamNamespaces, httpClientNamespaces, resolve) => {
|
|
58
64
|
if (node.callee.type === "Super") return Option.none();
|
|
59
65
|
const arity = retryArity(node.callee, effectNamespaces, streamNamespaces, httpClientNamespaces);
|
|
60
|
-
return Option.flatMap(Option.flatMap(arity, (count) => policyArgument(node, count)), policySchedule);
|
|
66
|
+
return Option.flatMap(Option.flatMap(arity, (count) => policyArgument(node, count)), (policy) => policySchedule(policy, resolve));
|
|
61
67
|
};
|
|
62
68
|
const noUnboundedRetry = define({
|
|
63
69
|
name: "no-unbounded-retry",
|
|
@@ -71,6 +77,7 @@ const noUnboundedRetry = define({
|
|
|
71
77
|
const httpClientNamespaces = new Set(["HttpClient"]);
|
|
72
78
|
const scheduleNamespaces = new Set(["Schedule"]);
|
|
73
79
|
const streamNamespaces = new Set(["Stream"]);
|
|
80
|
+
const resolve = constResolver(ctx);
|
|
74
81
|
return {
|
|
75
82
|
ImportDeclaration: (node) => {
|
|
76
83
|
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
@@ -82,15 +89,13 @@ const noUnboundedRetry = define({
|
|
|
82
89
|
},
|
|
83
90
|
CallExpression: (node) => {
|
|
84
91
|
if (node.type !== "CallExpression") return Effect.void;
|
|
85
|
-
const policy = retryPolicy(node, visibleNamespaces(ctx, node, effectNamespaces), visibleNamespaces(ctx, node, streamNamespaces), visibleNamespaces(ctx, node, httpClientNamespaces));
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}))
|
|
93
|
-
});
|
|
92
|
+
const policy = retryPolicy(node, visibleNamespaces(ctx, node, effectNamespaces), visibleNamespaces(ctx, node, streamNamespaces), visibleNamespaces(ctx, node, httpClientNamespaces), resolve);
|
|
93
|
+
const scheduleNamespacesInScope = visibleNamespaces(ctx, node, scheduleNamespaces);
|
|
94
|
+
if (!Option.exists(policy, (schedule) => isUnboundedSchedule(schedule, scheduleNamespacesInScope, resolve))) return Effect.void;
|
|
95
|
+
return ctx.report(make({
|
|
96
|
+
node,
|
|
97
|
+
message: "Bound retry attempts or elapsed time. Add times or compose the schedule with Schedule.recurs or Schedule.take."
|
|
98
|
+
}));
|
|
94
99
|
}
|
|
95
100
|
};
|
|
96
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-effect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "oxlint plugin with Effect-specific lint rules",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"lint:fix": "oxlint --fix",
|
|
52
52
|
"fmt": "oxfmt",
|
|
53
53
|
"fmt:check": "oxfmt --check",
|
|
54
|
-
"test": "bun run build && bun test",
|
|
55
|
-
"gate": "bun run build && concurrently -n type,style,self,generated,test -c blue,yellow,cyan,magenta,green \"bun run typecheck\" \"bun run lint\" \"bun run lint:self\" \"bun run codegen:check\" \"bun test\"",
|
|
54
|
+
"test": "bun run build && bun test --timeout=30000",
|
|
55
|
+
"gate": "bun run build && concurrently -n type,style,self,generated,test -c blue,yellow,cyan,magenta,green \"bun run typecheck\" \"bun run lint\" \"bun run lint:self\" \"bun run codegen:check\" \"bun test --timeout=30000\"",
|
|
56
56
|
"codegen": "bun run scripts/codegen.ts",
|
|
57
57
|
"codegen:check": "bun run scripts/codegen.ts --check",
|
|
58
58
|
"add-rule": "bun run scripts/add-rule.ts",
|