oxlint-plugin-effect 0.2.2 → 0.3.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 +91 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +13 -1
- package/dist/presets/core.d.ts +7 -0
- package/dist/presets/core.js +8 -1
- package/dist/presets/effect-native.d.ts +1 -0
- package/dist/presets/effect-native.js +1 -0
- package/dist/presets/full.d.ts +8 -0
- package/dist/presets/full.js +9 -1
- package/dist/presets/functional.d.ts +1 -0
- package/dist/presets/functional.js +2 -1
- package/dist/presets/strict.d.ts +9 -0
- package/dist/presets/strict.js +10 -1
- package/dist/rules/index.d.ts +11 -2
- package/dist/rules/index.js +19 -1
- package/dist/rules/no-dynamic-imports.d.ts +6 -0
- package/dist/rules/no-dynamic-imports.js +90 -0
- package/dist/rules/no-effect-all-step-sequencing.d.ts +6 -0
- package/dist/rules/no-effect-all-step-sequencing.js +63 -0
- package/dist/rules/no-hand-rolled-tagged-union.d.ts +6 -0
- package/dist/rules/no-hand-rolled-tagged-union.js +70 -0
- package/dist/rules/no-make-unsafe.d.ts +6 -0
- package/dist/rules/no-make-unsafe.js +30 -0
- package/dist/rules/no-manual-data-guard.d.ts +6 -0
- package/dist/rules/no-manual-data-guard.js +56 -0
- package/dist/rules/no-promise-control-flow-in-tests.d.ts +6 -0
- package/dist/rules/no-promise-control-flow-in-tests.js +94 -0
- package/dist/rules/no-schema-struct.d.ts +6 -0
- package/dist/rules/no-schema-struct.js +28 -0
- package/dist/rules/no-sleep-in-tests.d.ts +6 -0
- package/dist/rules/no-sleep-in-tests.js +55 -0
- package/dist/rules/no-spread.d.ts +6 -0
- package/dist/rules/no-spread.js +26 -0
- package/dist/rules/no-try-catch.js +3 -3
- package/dist/vendor/effect-oxlint/Rule.d.ts +6 -6
- package/dist/vendor/effect-oxlint/RuleContext.d.ts +1 -1
- package/dist/vendor/effect-oxlint/RuleContext.js +1 -1
- package/dist/vendor/effect-oxlint/Testing.js +4 -1
- package/dist/vendor/effect-oxlint/Visitor.d.ts +1 -1
- package/dist/vendor/effect-oxlint/index.d.ts +7 -1
- package/dist/vendor/effect-oxlint/index.js +24 -1
- package/package.json +23 -10
|
@@ -0,0 +1,63 @@
|
|
|
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 * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-effect-all-step-sequencing.ts
|
|
7
|
+
const isAstNode = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
8
|
+
const statefulSteps = new Set([
|
|
9
|
+
"Atom.set",
|
|
10
|
+
"Effect.log",
|
|
11
|
+
"Effect.logDebug",
|
|
12
|
+
"Effect.logError",
|
|
13
|
+
"Effect.logFatal",
|
|
14
|
+
"Effect.logInfo",
|
|
15
|
+
"Effect.logTrace",
|
|
16
|
+
"Effect.logWarning",
|
|
17
|
+
"Fiber.interrupt",
|
|
18
|
+
"Reactivity.invalidate",
|
|
19
|
+
"Ref.set",
|
|
20
|
+
"SubscriptionRef.set"
|
|
21
|
+
]);
|
|
22
|
+
const memberName = (value) => {
|
|
23
|
+
if (!isAstNode(value) || value.type !== "MemberExpression") return void 0;
|
|
24
|
+
const object = value["object"];
|
|
25
|
+
const property = value["property"];
|
|
26
|
+
if (!isAstNode(object) || !isAstNode(property)) return void 0;
|
|
27
|
+
if (object.type !== "Identifier" || property.type !== "Identifier") return void 0;
|
|
28
|
+
return typeof object["name"] === "string" && typeof property["name"] === "string" ? `${object["name"]}.${property["name"]}` : void 0;
|
|
29
|
+
};
|
|
30
|
+
const containsStatefulStep = (value) => {
|
|
31
|
+
if (Array.isArray(value)) return value.some(containsStatefulStep);
|
|
32
|
+
if (!isAstNode(value)) return false;
|
|
33
|
+
if (value.type === "CallExpression" && statefulSteps.has(memberName(value["callee"]) ?? "")) return true;
|
|
34
|
+
return Object.entries(value).some(([key, child]) => key !== "parent" && containsStatefulStep(child));
|
|
35
|
+
};
|
|
36
|
+
const hasSequentialConcurrency = (value) => {
|
|
37
|
+
if (!isAstNode(value) || value.type !== "ObjectExpression") return false;
|
|
38
|
+
return (Array.isArray(value["properties"]) ? value["properties"] : []).some((property) => {
|
|
39
|
+
if (!isAstNode(property) || property.type !== "Property") return false;
|
|
40
|
+
const key = property["key"];
|
|
41
|
+
const propertyValue = property["value"];
|
|
42
|
+
return isAstNode(key) && (key["name"] === "concurrency" || key["value"] === "concurrency") && isAstNode(propertyValue) && propertyValue["value"] === 1;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const noEffectAllStepSequencing = define({
|
|
46
|
+
name: "no-effect-all-step-sequencing",
|
|
47
|
+
meta: meta({
|
|
48
|
+
type: "suggestion",
|
|
49
|
+
description: "Use a linear Effect pipeline for sequential stateful steps."
|
|
50
|
+
}),
|
|
51
|
+
create: function* () {
|
|
52
|
+
const ctx = yield* RuleContext;
|
|
53
|
+
return { CallExpression: (node) => {
|
|
54
|
+
if (memberName(node.callee) !== "Effect.all" || node.arguments.length < 2) return Effect.void;
|
|
55
|
+
return hasSequentialConcurrency(node.arguments[1]) && containsStatefulStep(node.arguments[0]) ? ctx.report(make({
|
|
56
|
+
node,
|
|
57
|
+
message: "Avoid Effect.all for sequential side-effect steps. Use one explicit linear pipeline with Effect.andThen or Effect.flatMap."
|
|
58
|
+
})) : Effect.void;
|
|
59
|
+
} };
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
//#endregion
|
|
63
|
+
export { noEffectAllStepSequencing };
|
|
@@ -0,0 +1,70 @@
|
|
|
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 * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-hand-rolled-tagged-union.ts
|
|
7
|
+
const isAstNode = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
8
|
+
const getNodeField = (node, field) => {
|
|
9
|
+
const value = node[field];
|
|
10
|
+
return isAstNode(value) ? value : void 0;
|
|
11
|
+
};
|
|
12
|
+
const getStringField = (node, field) => {
|
|
13
|
+
const value = node[field];
|
|
14
|
+
return typeof value === "string" ? value : void 0;
|
|
15
|
+
};
|
|
16
|
+
const getNodeArrayField = (node, field) => {
|
|
17
|
+
const value = node[field];
|
|
18
|
+
return Array.isArray(value) ? value.filter(isAstNode) : void 0;
|
|
19
|
+
};
|
|
20
|
+
const getPropertyKeyName = (node) => {
|
|
21
|
+
if (node.type === "Identifier" || node.type === "StringLiteral" || node.type === "Literal") return getStringField(node, "name") ?? getStringField(node, "value");
|
|
22
|
+
};
|
|
23
|
+
const isPascalCaseTag = (value) => {
|
|
24
|
+
if (value.length === 0) return false;
|
|
25
|
+
const first = value.charAt(0);
|
|
26
|
+
return first === first.toUpperCase() && first !== first.toLowerCase();
|
|
27
|
+
};
|
|
28
|
+
const isReportableTagLiteral = (member) => {
|
|
29
|
+
if (member.type !== "TSPropertySignature") return false;
|
|
30
|
+
const key = getNodeField(member, "key");
|
|
31
|
+
if (key === void 0 || getPropertyKeyName(key) !== "_tag") return false;
|
|
32
|
+
const annotation = getNodeField(member, "typeAnnotation");
|
|
33
|
+
const inner = annotation === void 0 ? void 0 : getNodeField(annotation, "typeAnnotation");
|
|
34
|
+
if (inner?.type !== "TSLiteralType") return false;
|
|
35
|
+
const literal = getNodeField(inner, "literal");
|
|
36
|
+
if (literal === void 0) return false;
|
|
37
|
+
if (literal.type !== "StringLiteral" && literal.type !== "Literal") return false;
|
|
38
|
+
const value = getStringField(literal, "value");
|
|
39
|
+
return value !== void 0 && isPascalCaseTag(value);
|
|
40
|
+
};
|
|
41
|
+
const literalHasTag = (node) => {
|
|
42
|
+
if (node.type !== "TSTypeLiteral") return false;
|
|
43
|
+
const members = getNodeArrayField(node, "members");
|
|
44
|
+
return members !== void 0 && members.some(isReportableTagLiteral);
|
|
45
|
+
};
|
|
46
|
+
const noHandRolledTaggedUnion = define({
|
|
47
|
+
name: "no-hand-rolled-tagged-union",
|
|
48
|
+
meta: meta({
|
|
49
|
+
type: "suggestion",
|
|
50
|
+
description: "Use Schema.TaggedUnion, Schema.TaggedStruct, or Schema.TaggedErrorClass instead of inline _tag union literals."
|
|
51
|
+
}),
|
|
52
|
+
create: function* () {
|
|
53
|
+
const ctx = yield* RuleContext;
|
|
54
|
+
return { TSUnionType: (node) => {
|
|
55
|
+
const types = getNodeArrayField(node, "types");
|
|
56
|
+
if (types === void 0 || types.length < 2) return Effect.void;
|
|
57
|
+
let tagged = 0;
|
|
58
|
+
for (const type of types) {
|
|
59
|
+
if (literalHasTag(type)) tagged += 1;
|
|
60
|
+
if (tagged >= 2) return ctx.report(make({
|
|
61
|
+
node,
|
|
62
|
+
message: "Hand-rolled `_tag` discriminated union. Use Schema.TaggedUnion, Schema.TaggedStruct, or Schema.TaggedErrorClass instead."
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
return Effect.void;
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
//#endregion
|
|
70
|
+
export { noHandRolledTaggedUnion };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { memberNames } 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 "../vendor/effect-oxlint/index.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/no-make-unsafe.ts
|
|
9
|
+
const noMakeUnsafe = define({
|
|
10
|
+
name: "no-make-unsafe",
|
|
11
|
+
meta: meta({
|
|
12
|
+
type: "problem",
|
|
13
|
+
description: "Avoid makeUnsafe constructors. Use safe Effectful or Option-returning construction."
|
|
14
|
+
}),
|
|
15
|
+
create: function* () {
|
|
16
|
+
const ctx = yield* RuleContext;
|
|
17
|
+
return { CallExpression: (node) => {
|
|
18
|
+
const call = node;
|
|
19
|
+
if (call.callee.type !== "MemberExpression") return Effect.void;
|
|
20
|
+
if (Option.isNone(memberNames(call.callee))) return Effect.void;
|
|
21
|
+
if (Option.getOrUndefined(memberNames(call.callee))?.[1] !== "makeUnsafe") return Effect.void;
|
|
22
|
+
return ctx.report(make({
|
|
23
|
+
node,
|
|
24
|
+
message: "Do not call `.makeUnsafe(...)`. Use safe Effectful/Option-returning construction instead of bypassing validation."
|
|
25
|
+
}));
|
|
26
|
+
} };
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
//#endregion
|
|
30
|
+
export { noMakeUnsafe };
|
|
@@ -0,0 +1,56 @@
|
|
|
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 * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-manual-data-guard.ts
|
|
7
|
+
const isAstNode = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
8
|
+
const functionTypes = new Set([
|
|
9
|
+
"ArrowFunctionExpression",
|
|
10
|
+
"FunctionDeclaration",
|
|
11
|
+
"FunctionExpression"
|
|
12
|
+
]);
|
|
13
|
+
const findFunction = (node) => {
|
|
14
|
+
let parent = node.parent;
|
|
15
|
+
while (isAstNode(parent)) {
|
|
16
|
+
if (functionTypes.has(parent.type)) return parent;
|
|
17
|
+
parent = parent.parent;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const containsNodeType = (value, nodeTypes) => {
|
|
21
|
+
if (Array.isArray(value)) return value.some((item) => containsNodeType(item, nodeTypes));
|
|
22
|
+
if (!isAstNode(value)) return false;
|
|
23
|
+
if (nodeTypes.has(value.type)) return true;
|
|
24
|
+
return Object.entries(value).some(([key, child]) => key !== "parent" && containsNodeType(child, nodeTypes));
|
|
25
|
+
};
|
|
26
|
+
const containsInstanceOf = (value) => {
|
|
27
|
+
if (Array.isArray(value)) return value.some(containsInstanceOf);
|
|
28
|
+
if (!isAstNode(value)) return false;
|
|
29
|
+
if (value.type === "BinaryExpression" && value["operator"] === "instanceof") return true;
|
|
30
|
+
return Object.entries(value).some(([key, child]) => key !== "parent" && containsInstanceOf(child));
|
|
31
|
+
};
|
|
32
|
+
const parameterName = (parameter) => {
|
|
33
|
+
if (!isAstNode(parameter)) return void 0;
|
|
34
|
+
if ((parameter.type === "Identifier" || parameter.type === "BindingIdentifier") && typeof parameter["name"] === "string") return parameter["name"];
|
|
35
|
+
};
|
|
36
|
+
const noManualDataGuard = define({
|
|
37
|
+
name: "no-manual-data-guard",
|
|
38
|
+
meta: meta({
|
|
39
|
+
type: "suggestion",
|
|
40
|
+
description: "Decode unknown data with Schema instead of asserting it with a type predicate."
|
|
41
|
+
}),
|
|
42
|
+
create: function* () {
|
|
43
|
+
const ctx = yield* RuleContext;
|
|
44
|
+
return { TSTypePredicate: (node) => {
|
|
45
|
+
const fn = findFunction(node);
|
|
46
|
+
if (fn === void 0 || containsInstanceOf(fn["body"])) return Effect.void;
|
|
47
|
+
const predicateParameter = parameterName(node.parameterName);
|
|
48
|
+
return (Array.isArray(fn["params"]) ? fn["params"] : []).some((parameter) => parameterName(parameter) === predicateParameter && containsNodeType(parameter, new Set(["TSUnknownKeyword", "TSAnyKeyword"]))) ? ctx.report(make({
|
|
49
|
+
node,
|
|
50
|
+
message: "Avoid promoting unknown data with a manual type predicate. Decode it with the canonical Schema first."
|
|
51
|
+
})) : Effect.void;
|
|
52
|
+
} };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
//#endregion
|
|
56
|
+
export { noManualDataGuard };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { memberNames } 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 "../vendor/effect-oxlint/index.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/no-promise-control-flow-in-tests.ts
|
|
9
|
+
const isTestFilename = (filename) => /\.test\.tsx?$/.test(filename) || /\/tests\/.*\.[cm]?tsx?$/.test(filename);
|
|
10
|
+
const promiseChainMethods = new Set([
|
|
11
|
+
"then",
|
|
12
|
+
"catch",
|
|
13
|
+
"finally"
|
|
14
|
+
]);
|
|
15
|
+
const promiseStaticMethods = new Set([
|
|
16
|
+
"all",
|
|
17
|
+
"allSettled",
|
|
18
|
+
"any",
|
|
19
|
+
"race",
|
|
20
|
+
"resolve",
|
|
21
|
+
"reject"
|
|
22
|
+
]);
|
|
23
|
+
const isAsyncFunction = (node) => "async" in node && node.async === true;
|
|
24
|
+
const promiseChainMethodName = (node) => {
|
|
25
|
+
if (node.callee.type !== "MemberExpression") return void 0;
|
|
26
|
+
const names = Option.getOrUndefined(memberNames(node.callee));
|
|
27
|
+
if (names === void 0) return void 0;
|
|
28
|
+
const [object, property] = names;
|
|
29
|
+
if (object === "Effect") return void 0;
|
|
30
|
+
return promiseChainMethods.has(property) ? property : void 0;
|
|
31
|
+
};
|
|
32
|
+
const noPromiseControlFlowInTests = define({
|
|
33
|
+
name: "no-promise-control-flow-in-tests",
|
|
34
|
+
meta: meta({
|
|
35
|
+
type: "problem",
|
|
36
|
+
description: "Keep test control flow in Effect instead of async/await, try/finally, or Promise chains."
|
|
37
|
+
}),
|
|
38
|
+
create: function* () {
|
|
39
|
+
const ctx = yield* RuleContext;
|
|
40
|
+
if (!isTestFilename(ctx.filename)) return {};
|
|
41
|
+
return {
|
|
42
|
+
TryStatement: (node) => {
|
|
43
|
+
if (!("finalizer" in node) || node.finalizer == null) return Effect.void;
|
|
44
|
+
return ctx.report(make({
|
|
45
|
+
node,
|
|
46
|
+
message: "Do not use `try/finally` cleanup in tests. Use Effect scopes."
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
FunctionDeclaration: (node) => isAsyncFunction(node) ? ctx.report(make({
|
|
50
|
+
node,
|
|
51
|
+
message: "Do not use async test functions. Return an Effect from the test body."
|
|
52
|
+
})) : Effect.void,
|
|
53
|
+
FunctionExpression: (node) => isAsyncFunction(node) ? ctx.report(make({
|
|
54
|
+
node,
|
|
55
|
+
message: "Do not use async test functions. Return an Effect from the test body."
|
|
56
|
+
})) : Effect.void,
|
|
57
|
+
ArrowFunctionExpression: (node) => isAsyncFunction(node) ? ctx.report(make({
|
|
58
|
+
node,
|
|
59
|
+
message: "Do not use async test functions. Return an Effect from the test body."
|
|
60
|
+
})) : Effect.void,
|
|
61
|
+
AwaitExpression: (node) => ctx.report(make({
|
|
62
|
+
node,
|
|
63
|
+
message: "Do not use `await` in tests. Use `yield*` inside Effect.gen."
|
|
64
|
+
})),
|
|
65
|
+
NewExpression: (node) => {
|
|
66
|
+
const expression = node;
|
|
67
|
+
if (expression.callee.type !== "Identifier" || expression.callee.name !== "Promise") return Effect.void;
|
|
68
|
+
return ctx.report(make({
|
|
69
|
+
node,
|
|
70
|
+
message: "Do not construct Promise directly in tests. Use Effect.async/Effect.promise at real boundaries."
|
|
71
|
+
}));
|
|
72
|
+
},
|
|
73
|
+
CallExpression: (node) => {
|
|
74
|
+
const call = node;
|
|
75
|
+
const chainMethod = promiseChainMethodName(call);
|
|
76
|
+
if (chainMethod !== void 0) return ctx.report(make({
|
|
77
|
+
node,
|
|
78
|
+
message: `Do not use Promise.${chainMethod} style chains in tests. Keep control flow in Effect.`
|
|
79
|
+
}));
|
|
80
|
+
if (call.callee.type !== "MemberExpression") return Effect.void;
|
|
81
|
+
const names = Option.getOrUndefined(memberNames(call.callee));
|
|
82
|
+
if (names === void 0) return Effect.void;
|
|
83
|
+
const [object, property] = names;
|
|
84
|
+
if (object !== "Promise" || !promiseStaticMethods.has(property)) return Effect.void;
|
|
85
|
+
return ctx.report(make({
|
|
86
|
+
node,
|
|
87
|
+
message: `Do not use Promise.${property} in tests. Use Effect concurrency primitives.`
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
//#endregion
|
|
94
|
+
export { noPromiseControlFlowInTests };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { matchCallOf } 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 "../vendor/effect-oxlint/index.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/no-schema-struct.ts
|
|
9
|
+
const noSchemaStruct = define({
|
|
10
|
+
name: "no-schema-struct",
|
|
11
|
+
meta: meta({
|
|
12
|
+
type: "suggestion",
|
|
13
|
+
description: "Use Schema.Class instead of Schema.Struct."
|
|
14
|
+
}),
|
|
15
|
+
create: function* () {
|
|
16
|
+
const ctx = yield* RuleContext;
|
|
17
|
+
return { CallExpression: (node) => {
|
|
18
|
+
const call = node;
|
|
19
|
+
if (Option.isNone(matchCallOf(call, "Schema", "Struct"))) return Effect.void;
|
|
20
|
+
return ctx.report(make({
|
|
21
|
+
node,
|
|
22
|
+
message: "Use Schema.Class instead of Schema.Struct."
|
|
23
|
+
}));
|
|
24
|
+
} };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
//#endregion
|
|
28
|
+
export { noSchemaStruct };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { memberNames } 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 "../vendor/effect-oxlint/index.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/rules/no-sleep-in-tests.ts
|
|
9
|
+
const allowPattern = /\beffect\/no-sleep-in-tests:\s*allow\s+\S/;
|
|
10
|
+
const isTestFilename = (filename) => /\.test\.tsx?$/.test(filename) || /\/tests\/.*\.[cm]?tsx?$/.test(filename);
|
|
11
|
+
const getLine = (node, edge) => {
|
|
12
|
+
const loc = node["loc"];
|
|
13
|
+
if (typeof loc !== "object" || loc === null) return void 0;
|
|
14
|
+
const point = loc[edge];
|
|
15
|
+
if (typeof point !== "object" || point === null) return void 0;
|
|
16
|
+
const line = point["line"];
|
|
17
|
+
return typeof line === "number" ? line : void 0;
|
|
18
|
+
};
|
|
19
|
+
const hasAllowComment = (node, comments) => {
|
|
20
|
+
const startLine = getLine(node, "start");
|
|
21
|
+
if (startLine === void 0) return false;
|
|
22
|
+
return comments.some((comment) => {
|
|
23
|
+
const endLine = getLine(comment, "end");
|
|
24
|
+
if (endLine === void 0) return false;
|
|
25
|
+
if (endLine !== startLine - 1 && endLine !== startLine) return false;
|
|
26
|
+
return allowPattern.test(comment.value);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
const noSleepInTests = define({
|
|
30
|
+
name: "no-sleep-in-tests",
|
|
31
|
+
meta: meta({
|
|
32
|
+
type: "problem",
|
|
33
|
+
description: "Avoid fixed sleeps in tests. Use deterministic synchronization."
|
|
34
|
+
}),
|
|
35
|
+
create: function* () {
|
|
36
|
+
const ctx = yield* RuleContext;
|
|
37
|
+
if (!isTestFilename(ctx.filename)) return {};
|
|
38
|
+
return { CallExpression: (node) => {
|
|
39
|
+
const call = node;
|
|
40
|
+
if (call.callee.type !== "MemberExpression") return Effect.void;
|
|
41
|
+
const names = Option.getOrUndefined(memberNames(call.callee));
|
|
42
|
+
if (names === void 0) return Effect.void;
|
|
43
|
+
const [object, property] = names;
|
|
44
|
+
if (property !== "sleep") return Effect.void;
|
|
45
|
+
if (object !== "Effect" && object !== "Bun") return Effect.void;
|
|
46
|
+
if (hasAllowComment(node, ctx.sourceCode.getAllComments())) return Effect.void;
|
|
47
|
+
return ctx.report(make({
|
|
48
|
+
node,
|
|
49
|
+
message: `Avoid ${object}.sleep(...) in tests. Wait on Deferred, polling helpers, or test controls instead of fixed delays.`
|
|
50
|
+
}));
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
//#endregion
|
|
55
|
+
export { noSleepInTests };
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
//#region src/rules/no-spread.ts
|
|
6
|
+
const message = "Avoid spread syntax. Use explicit construction or a named transformation instead.";
|
|
7
|
+
const noSpread = define({
|
|
8
|
+
name: "no-spread",
|
|
9
|
+
meta: meta({
|
|
10
|
+
type: "suggestion",
|
|
11
|
+
description: message
|
|
12
|
+
}),
|
|
13
|
+
create: function* () {
|
|
14
|
+
const ctx = yield* RuleContext;
|
|
15
|
+
const reportSpread = (node) => ctx.report(make({
|
|
16
|
+
node,
|
|
17
|
+
message
|
|
18
|
+
}));
|
|
19
|
+
return {
|
|
20
|
+
SpreadElement: reportSpread,
|
|
21
|
+
JSXSpreadAttribute: reportSpread
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
//#endregion
|
|
26
|
+
export { noSpread };
|
|
@@ -8,7 +8,7 @@ import * as Effect from "effect/Effect";
|
|
|
8
8
|
import * as Ref from "effect/Ref";
|
|
9
9
|
//#region src/rules/no-try-catch.ts
|
|
10
10
|
/**
|
|
11
|
-
* Ban `try
|
|
11
|
+
* Ban all `try` statements: try/catch, try/finally, and try/catch/finally.
|
|
12
12
|
*
|
|
13
13
|
* Inside Effect.gen/fn: "Use Effect.try or Effect.tryPromise"
|
|
14
14
|
* Outside: "Wrap with Effect.fn and use Effect.try for error handling"
|
|
@@ -19,14 +19,14 @@ const noTryCatch = define({
|
|
|
19
19
|
name: "no-try-catch",
|
|
20
20
|
meta: meta({
|
|
21
21
|
type: "suggestion",
|
|
22
|
-
description: "Avoid try/catch. Use Effect
|
|
22
|
+
description: "Avoid try/catch/finally blocks. Use Effect error handling and finalization operators."
|
|
23
23
|
}),
|
|
24
24
|
create: function* () {
|
|
25
25
|
const ctx = yield* RuleContext;
|
|
26
26
|
const [depth, tracker] = yield* makeEffectContextTracker;
|
|
27
27
|
return merge(tracker, { TryStatement: (node) => Effect.flatMap(Ref.get(depth), (d) => ctx.report(make({
|
|
28
28
|
node,
|
|
29
|
-
message: d > 0 ? "Avoid try/catch inside Effect.gen/fn. Use Effect.try
|
|
29
|
+
message: d > 0 ? "Avoid try/catch/finally inside Effect.gen/fn. Use Effect.try or Effect.tryPromise for failures and Effect.ensuring or Effect.acquireUseRelease for finalization." : "Avoid try/catch/finally. Use Effect.try or Effect.tryPromise for failures and Effect.ensuring or Effect.acquireUseRelease for finalization."
|
|
30
30
|
}))) });
|
|
31
31
|
}
|
|
32
32
|
});
|
|
@@ -58,7 +58,7 @@ declare const define: <Options = undefined>(config: RuleConfig<Options>) => Crea
|
|
|
58
58
|
*
|
|
59
59
|
* @since 0.2.0
|
|
60
60
|
*/
|
|
61
|
-
type EffectVersion =
|
|
61
|
+
type EffectVersion = "v3" | "v4" | "both";
|
|
62
62
|
/**
|
|
63
63
|
* Build `RuleMeta` with sensible defaults.
|
|
64
64
|
*
|
|
@@ -168,24 +168,24 @@ declare const banStatement: (nodeType: string, opts: {
|
|
|
168
168
|
* @since 0.3.0
|
|
169
169
|
*/
|
|
170
170
|
type BanSpec = {
|
|
171
|
-
readonly type:
|
|
171
|
+
readonly type: "member";
|
|
172
172
|
readonly object: string;
|
|
173
173
|
readonly property: string | ReadonlyArray<string>;
|
|
174
174
|
readonly message: string;
|
|
175
175
|
} | {
|
|
176
|
-
readonly type:
|
|
176
|
+
readonly type: "call";
|
|
177
177
|
readonly name: string | ReadonlyArray<string>;
|
|
178
178
|
readonly message: string;
|
|
179
179
|
} | {
|
|
180
|
-
readonly type:
|
|
180
|
+
readonly type: "new";
|
|
181
181
|
readonly name: string | ReadonlyArray<string>;
|
|
182
182
|
readonly message: string;
|
|
183
183
|
} | {
|
|
184
|
-
readonly type:
|
|
184
|
+
readonly type: "import";
|
|
185
185
|
readonly source: string | ((source: string) => boolean);
|
|
186
186
|
readonly message: string;
|
|
187
187
|
} | {
|
|
188
|
-
readonly type:
|
|
188
|
+
readonly type: "statement";
|
|
189
189
|
readonly nodeType: string;
|
|
190
190
|
readonly message: string;
|
|
191
191
|
};
|
|
@@ -3,7 +3,7 @@ import * as Context$1 from "effect/Context";
|
|
|
3
3
|
import { Context, Diagnostic, ESTree, LanguageOptions, Options, Settings, SourceCode } from "@oxlint/plugins";
|
|
4
4
|
|
|
5
5
|
//#region src/vendor/effect-oxlint/RuleContext.d.ts
|
|
6
|
-
declare const RuleContext_base: Context$1.ServiceClass<RuleContext, "effect-oxlint/RuleContext", {
|
|
6
|
+
declare const RuleContext_base: Context$1.ServiceClass<RuleContext, "oxlint-plugin-effect/vendor/effect-oxlint/RuleContext", {
|
|
7
7
|
/** Report a lint diagnostic for the current file. */readonly report: (diagnostic: Diagnostic) => Effect.Effect<void>; /** Rule ID in `plugin/rule` form. */
|
|
8
8
|
readonly id: string; /** Absolute path of the file being linted. */
|
|
9
9
|
readonly filename: string; /** Current working directory. */
|
|
@@ -9,7 +9,7 @@ import * as Context from "effect/Context";
|
|
|
9
9
|
*
|
|
10
10
|
* @since 0.1.0
|
|
11
11
|
*/
|
|
12
|
-
var RuleContext = class extends Context.Service()("effect-oxlint/RuleContext") {};
|
|
12
|
+
var RuleContext = class extends Context.Service()("oxlint-plugin-effect/vendor/effect-oxlint/RuleContext") {};
|
|
13
13
|
/**
|
|
14
14
|
* Build a `RuleContext` value from the raw oxlint `Context`.
|
|
15
15
|
*
|
|
@@ -866,7 +866,10 @@ const mockRuleContextLayer = (opts) => {
|
|
|
866
866
|
*
|
|
867
867
|
* @since 0.2.0
|
|
868
868
|
*/
|
|
869
|
-
const withMockRuleContext = (effect, opts) =>
|
|
869
|
+
const withMockRuleContext = (effect, opts) => {
|
|
870
|
+
const { context } = createMockContext(opts);
|
|
871
|
+
return Effect.provideService(effect, RuleContext, fromOxlintContext(context));
|
|
872
|
+
};
|
|
870
873
|
/** @internal Call a visitor handler on a mock AST node. */
|
|
871
874
|
const callHandler = (visitors, key, visitorNode) => {
|
|
872
875
|
const handler = visitors[key];
|
|
@@ -18,7 +18,7 @@ declare namespace Visitor_d_exports {
|
|
|
18
18
|
*/
|
|
19
19
|
type EffectHandler<N = ESTree.Node> = {
|
|
20
20
|
bivarianceHack(node: N): Effect.Effect<void, never, RuleContext>;
|
|
21
|
-
}[
|
|
21
|
+
}["bivarianceHack"];
|
|
22
22
|
/**
|
|
23
23
|
* A map from AST node type names (and `"NodeType:exit"` variants) to
|
|
24
24
|
* effectful handlers.
|
|
@@ -10,4 +10,10 @@ import { SourceCode_d_exports } from "./SourceCode.js";
|
|
|
10
10
|
import { Testing_d_exports } from "./Testing.js";
|
|
11
11
|
import { Token_d_exports } from "./Token.js";
|
|
12
12
|
import { Comment as OxlintComment, Context, CreateOnceRule, CreateRule, Definition, DefinitionType, ESTree, Fix, FixFn, Fixer, LineColumn, Location, Plugin as OxlintPlugin, Range, Ranged, Reference, Rule as OxlintRule, RuleDocs, RuleMeta, Scope as OxlintScope, ScopeManager, ScopeType, Settings, SourceCode as OxlintSourceCode, Span, Suggestion, Token as OxlintToken, Variable, Visitor as OxlintVisitor } from "@oxlint/plugins";
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
//#region src/vendor/effect-oxlint/index.d.ts
|
|
15
|
+
declare namespace index_d_exports {
|
|
16
|
+
export { AST_d_exports as AST, Comment_d_exports as Comment, Context, CreateOnceRule, CreateRule, Definition, DefinitionType, Diagnostic_d_exports as Diagnostic, ESTree, Fix, FixFn, Fixer, LineColumn, Location, OxlintComment, OxlintPlugin, OxlintRule, OxlintScope, OxlintSourceCode, OxlintToken, OxlintVisitor, Plugin_d_exports as Plugin, Range, Ranged, Reference, Rule_d_exports as Rule, RuleContext, RuleDocs, RuleMeta, Scope_d_exports as Scope, ScopeManager, ScopeType, Settings, SourceCode_d_exports as SourceCode, Span, Suggestion, Testing_d_exports as Testing, Token_d_exports as Token, Variable, Visitor_d_exports as Visitor, ast, cwd, filename, id, report, sourceCode, text };
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { AST_d_exports as AST, Comment_d_exports as Comment, type Context, type CreateOnceRule, type CreateRule, type Definition, type DefinitionType, Diagnostic_d_exports as Diagnostic, type ESTree, type Fix, type FixFn, type Fixer, type LineColumn, type Location, type OxlintComment, type OxlintPlugin, type OxlintRule, type OxlintScope, type OxlintSourceCode, type OxlintToken, type OxlintVisitor, Plugin_d_exports as Plugin, type Range, type Ranged, type Reference, Rule_d_exports as Rule, RuleContext, type RuleDocs, type RuleMeta, Scope_d_exports as Scope, type ScopeManager, type ScopeType, type Settings, SourceCode_d_exports as SourceCode, type Span, type Suggestion, Testing_d_exports as Testing, Token_d_exports as Token, type Variable, Visitor_d_exports as Visitor, ast, cwd, filename, id, index_d_exports, report, sourceCode, text };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { __exportAll } from "../../_virtual/_rolldown/runtime.js";
|
|
1
2
|
import { AST_exports } from "./AST.js";
|
|
2
3
|
import { Comment_exports } from "./Comment.js";
|
|
3
4
|
import { Diagnostic_exports } from "./Diagnostic.js";
|
|
@@ -9,4 +10,26 @@ import { Scope_exports } from "./Scope.js";
|
|
|
9
10
|
import { SourceCode_exports } from "./SourceCode.js";
|
|
10
11
|
import { Testing_exports } from "./Testing.js";
|
|
11
12
|
import { Token_exports } from "./Token.js";
|
|
12
|
-
|
|
13
|
+
//#region src/vendor/effect-oxlint/index.ts
|
|
14
|
+
var effect_oxlint_exports = /* @__PURE__ */ __exportAll({
|
|
15
|
+
AST: () => AST_exports,
|
|
16
|
+
Comment: () => Comment_exports,
|
|
17
|
+
Diagnostic: () => Diagnostic_exports,
|
|
18
|
+
Plugin: () => Plugin_exports,
|
|
19
|
+
Rule: () => Rule_exports,
|
|
20
|
+
RuleContext: () => RuleContext,
|
|
21
|
+
Scope: () => Scope_exports,
|
|
22
|
+
SourceCode: () => SourceCode_exports,
|
|
23
|
+
Testing: () => Testing_exports,
|
|
24
|
+
Token: () => Token_exports,
|
|
25
|
+
Visitor: () => Visitor_exports,
|
|
26
|
+
ast: () => ast,
|
|
27
|
+
cwd: () => cwd,
|
|
28
|
+
filename: () => filename,
|
|
29
|
+
id: () => id,
|
|
30
|
+
report: () => report,
|
|
31
|
+
sourceCode: () => sourceCode,
|
|
32
|
+
text: () => text
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
export { AST_exports as AST, Comment_exports as Comment, Diagnostic_exports as Diagnostic, Plugin_exports as Plugin, Rule_exports as Rule, RuleContext, Scope_exports as Scope, SourceCode_exports as SourceCode, Testing_exports as Testing, Token_exports as Token, Visitor_exports as Visitor, ast, cwd, effect_oxlint_exports, filename, id, report, sourceCode, text };
|