oxlint-plugin-effect 0.8.1 → 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/README.md +21 -18
- package/dist/presets/recommended.d.ts +3 -0
- package/dist/presets/recommended.js +4 -1
- package/dist/rules/_tagged-control-flow.js +79 -0
- package/dist/rules/index.d.ts +4 -1
- package/dist/rules/index.js +7 -1
- package/dist/rules/prefer-catch-tag.d.ts +6 -0
- package/dist/rules/prefer-catch-tag.js +48 -0
- package/dist/rules/prefer-effect-fn.js +41 -13
- package/dist/rules/prefer-match-tags-exhaustive.d.ts +6 -0
- package/dist/rules/prefer-match-tags-exhaustive.js +46 -0
- package/dist/rules/prefer-predicate-is-tagged.d.ts +6 -0
- package/dist/rules/prefer-predicate-is-tagged.js +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,23 +25,26 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
|
|
|
25
25
|
|
|
26
26
|
## Recommended Rules
|
|
27
27
|
|
|
28
|
-
| Rule
|
|
29
|
-
|
|
|
30
|
-
| `effect/noAs`
|
|
31
|
-
| `effect/noAsyncFunction`
|
|
32
|
-
| `effect/noTryCatch`
|
|
33
|
-
| `effect/noTestLifecycleHooks`
|
|
34
|
-
| `effect/noThrowStatement`
|
|
35
|
-
| `effect/noNewPromise`
|
|
36
|
-
| `effect/noNewError`
|
|
37
|
-
| `effect/noNullish`
|
|
38
|
-
| `effect/noTernary`
|
|
39
|
-
| `effect/noDynamicImports`
|
|
40
|
-
| `effect/noEffectDo`
|
|
41
|
-
| `effect/noEffectBind`
|
|
42
|
-
| `effect/
|
|
43
|
-
| `effect/
|
|
44
|
-
| `effect/
|
|
28
|
+
| Rule | Contract |
|
|
29
|
+
| ---------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
30
|
+
| `effect/noAs` | Bans TypeScript `as` assertions; use `satisfies` |
|
|
31
|
+
| `effect/noAsyncFunction` | Bans async functions and await expressions |
|
|
32
|
+
| `effect/noTryCatch` | Bans every try/catch/finally statement |
|
|
33
|
+
| `effect/noTestLifecycleHooks` | Bans `beforeEach`, `afterEach`, `beforeAll`, and `afterAll`; use Effect scopes instead |
|
|
34
|
+
| `effect/noThrowStatement` | Bans every throw statement |
|
|
35
|
+
| `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
|
|
36
|
+
| `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
|
|
37
|
+
| `effect/noNullish` | Bans null and undefined; use Option or a domain enum for richer state |
|
|
38
|
+
| `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
|
|
39
|
+
| `effect/noDynamicImports` | Allows import() only behind a named lazy-loading boundary; bans require() |
|
|
40
|
+
| `effect/noEffectDo` | Bans Effect.Do |
|
|
41
|
+
| `effect/noEffectBind` | Bans Effect.bind |
|
|
42
|
+
| `effect/preferCatchTag` | Replaces manual `_tag` predicates in `Effect.catchIf` with tagged recovery |
|
|
43
|
+
| `effect/preferEffectFn` | Requires `Effect.fn` for a generator operation that adds a span |
|
|
44
|
+
| `effect/preferMatchTagsExhaustive` | Requires exhaustive `Match` for return-only `_tag` switches |
|
|
45
|
+
| `effect/preferPredicateIsTagged` | Replaces combined `_tag` comparisons with a named `Predicate` refinement |
|
|
46
|
+
| `effect/noGlobals` | Bans ambient capabilities with direct Effect replacements; allows `process.std*.isTTY` |
|
|
47
|
+
| `effect/noNodeBuiltinImport` | Bans fully replaced Node modules and replaced operations from partial modules |
|
|
45
48
|
|
|
46
49
|
## Anti-Slop Rules
|
|
47
50
|
|
|
@@ -63,7 +66,7 @@ The recommended preset also includes these rules from
|
|
|
63
66
|
|
|
64
67
|
See `THIRD_PARTY_NOTICES.md` for the source revision and license.
|
|
65
68
|
|
|
66
|
-
The preset intentionally allows `Effect.as`, `Option.as`, `Effect.never`, `Effect.async`,
|
|
69
|
+
The preset intentionally allows `Effect.as`, `Option.as`, `Effect.never`, `Effect.async`, simple `_tag` guards, partial or stateful `switch` statements, and runtime runners at explicit application boundaries.
|
|
67
70
|
|
|
68
71
|
## Platform Boundaries
|
|
69
72
|
|
|
@@ -29,7 +29,10 @@ declare const recommended: {
|
|
|
29
29
|
readonly "effect/noUnknownTypeAliases": "error";
|
|
30
30
|
readonly "effect/noUnsafeDictionaryType": "error";
|
|
31
31
|
readonly "effect/noWidenThenAssert": "error";
|
|
32
|
+
readonly "effect/preferCatchTag": "error";
|
|
32
33
|
readonly "effect/preferEffectFn": "error";
|
|
34
|
+
readonly "effect/preferMatchTagsExhaustive": "error";
|
|
35
|
+
readonly "effect/preferPredicateIsTagged": "error";
|
|
33
36
|
};
|
|
34
37
|
//#endregion
|
|
35
38
|
export { recommended };
|
|
@@ -29,7 +29,10 @@ const recommended = {
|
|
|
29
29
|
"effect/noUnknownTypeAliases": "error",
|
|
30
30
|
"effect/noUnsafeDictionaryType": "error",
|
|
31
31
|
"effect/noWidenThenAssert": "error",
|
|
32
|
-
"effect/
|
|
32
|
+
"effect/preferCatchTag": "error",
|
|
33
|
+
"effect/preferEffectFn": "error",
|
|
34
|
+
"effect/preferMatchTagsExhaustive": "error",
|
|
35
|
+
"effect/preferPredicateIsTagged": "error"
|
|
33
36
|
};
|
|
34
37
|
//#endregion
|
|
35
38
|
export { recommended };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/rules/_tagged-control-flow.ts
|
|
2
|
+
const expressionKey = (node) => {
|
|
3
|
+
if (node.type === "Identifier") return node.name;
|
|
4
|
+
if (node.type === "ThisExpression") return "this";
|
|
5
|
+
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return null;
|
|
6
|
+
const object = expressionKey(node.object);
|
|
7
|
+
if (object === null) return null;
|
|
8
|
+
return `${object}.${node.property.name}`;
|
|
9
|
+
};
|
|
10
|
+
const stringLiteral = (node) => {
|
|
11
|
+
if (node.type !== "Literal") return null;
|
|
12
|
+
if (typeof node.value !== "string") return null;
|
|
13
|
+
return node.value;
|
|
14
|
+
};
|
|
15
|
+
const taggedSubject = (node) => {
|
|
16
|
+
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier" || node.property.name !== "_tag") return null;
|
|
17
|
+
return expressionKey(node.object);
|
|
18
|
+
};
|
|
19
|
+
const tagComparison = (node) => {
|
|
20
|
+
if (node.type !== "BinaryExpression" || node.operator !== "===") return null;
|
|
21
|
+
const leftSubject = taggedSubject(node.left);
|
|
22
|
+
const rightTag = stringLiteral(node.right);
|
|
23
|
+
if (leftSubject !== null && rightTag !== null) return {
|
|
24
|
+
subject: leftSubject,
|
|
25
|
+
tag: rightTag
|
|
26
|
+
};
|
|
27
|
+
const rightSubject = taggedSubject(node.right);
|
|
28
|
+
const leftTag = stringLiteral(node.left);
|
|
29
|
+
if (rightSubject === null || leftTag === null) return null;
|
|
30
|
+
return {
|
|
31
|
+
subject: rightSubject,
|
|
32
|
+
tag: leftTag
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const tagComparisonsInOr = (node) => {
|
|
36
|
+
if (node.type === "LogicalExpression" && node.operator === "||") {
|
|
37
|
+
const left = tagComparisonsInOr(node.left);
|
|
38
|
+
const right = tagComparisonsInOr(node.right);
|
|
39
|
+
if (left === null || right === null) return null;
|
|
40
|
+
return [...left, ...right];
|
|
41
|
+
}
|
|
42
|
+
const comparison = tagComparison(node);
|
|
43
|
+
if (comparison === null) return null;
|
|
44
|
+
return [comparison];
|
|
45
|
+
};
|
|
46
|
+
const hasOneTaggedSubject = (comparisons) => {
|
|
47
|
+
const first = comparisons[0];
|
|
48
|
+
if (first === void 0 || comparisons.length < 2) return false;
|
|
49
|
+
return comparisons.every((comparison) => comparison.subject === first.subject);
|
|
50
|
+
};
|
|
51
|
+
const isEffectCall = (node, operation) => node.callee.type !== "Super" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.object.type === "Identifier" && node.callee.object.name === "Effect" && node.callee.property.type === "Identifier" && node.callee.property.name === operation;
|
|
52
|
+
const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
53
|
+
const effectCallbackLocation = (node, operation) => {
|
|
54
|
+
let current = node.parent;
|
|
55
|
+
while (current !== void 0 && current !== null) {
|
|
56
|
+
if (isFunction(current)) {
|
|
57
|
+
const call = current.parent;
|
|
58
|
+
if (call?.type !== "CallExpression" || !isEffectCall(call, operation)) return null;
|
|
59
|
+
const index = call.arguments.indexOf(current);
|
|
60
|
+
if (index < 0) return null;
|
|
61
|
+
return {
|
|
62
|
+
argumentCount: call.arguments.length,
|
|
63
|
+
index
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
current = current.parent;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
const isInsideCatchAllHandler = (node) => {
|
|
71
|
+
const location = effectCallbackLocation(node, "catchAll");
|
|
72
|
+
if (location === null) return false;
|
|
73
|
+
let handlerIndex = 1;
|
|
74
|
+
if (location.argumentCount === 1) handlerIndex = 0;
|
|
75
|
+
return location.index === handlerIndex;
|
|
76
|
+
};
|
|
77
|
+
const taggedSwitchSubject = (node) => taggedSubject(node.discriminant);
|
|
78
|
+
//#endregion
|
|
79
|
+
export { effectCallbackLocation, hasOneTaggedSubject, isEffectCall, isInsideCatchAllHandler, tagComparisonsInOr, taggedSwitchSubject };
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -22,11 +22,14 @@ import { noUnknownParameters } from "./no-unknown-parameters.js";
|
|
|
22
22
|
import { noUnknownTypeAliases } from "./no-unknown-type-aliases.js";
|
|
23
23
|
import { noUnsafeDictionaryType } from "./no-unsafe-dictionary-type.js";
|
|
24
24
|
import { noWidenThenAssert } from "./no-widen-then-assert.js";
|
|
25
|
+
import { preferCatchTag } from "./prefer-catch-tag.js";
|
|
25
26
|
import { preferEffectFn } from "./prefer-effect-fn.js";
|
|
27
|
+
import { preferMatchTagsExhaustive } from "./prefer-match-tags-exhaustive.js";
|
|
28
|
+
import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
|
|
26
29
|
|
|
27
30
|
//#region src/rules/index.d.ts
|
|
28
31
|
declare namespace index_d_exports {
|
|
29
|
-
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, preferEffectFn };
|
|
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 };
|
|
30
33
|
}
|
|
31
34
|
//#endregion
|
|
32
35
|
export { index_d_exports };
|
package/dist/rules/index.js
CHANGED
|
@@ -23,7 +23,10 @@ import { noUnknownParameters } from "./no-unknown-parameters.js";
|
|
|
23
23
|
import { noUnknownTypeAliases } from "./no-unknown-type-aliases.js";
|
|
24
24
|
import { noUnsafeDictionaryType } from "./no-unsafe-dictionary-type.js";
|
|
25
25
|
import { noWidenThenAssert } from "./no-widen-then-assert.js";
|
|
26
|
+
import { preferCatchTag } from "./prefer-catch-tag.js";
|
|
26
27
|
import { preferEffectFn } from "./prefer-effect-fn.js";
|
|
28
|
+
import { preferMatchTagsExhaustive } from "./prefer-match-tags-exhaustive.js";
|
|
29
|
+
import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
|
|
27
30
|
//#region src/rules/index.ts
|
|
28
31
|
var rules_exports = /* @__PURE__ */ __exportAll({
|
|
29
32
|
noAs: () => noAs,
|
|
@@ -50,7 +53,10 @@ var rules_exports = /* @__PURE__ */ __exportAll({
|
|
|
50
53
|
noUnknownTypeAliases: () => noUnknownTypeAliases,
|
|
51
54
|
noUnsafeDictionaryType: () => noUnsafeDictionaryType,
|
|
52
55
|
noWidenThenAssert: () => noWidenThenAssert,
|
|
53
|
-
|
|
56
|
+
preferCatchTag: () => preferCatchTag,
|
|
57
|
+
preferEffectFn: () => preferEffectFn,
|
|
58
|
+
preferMatchTagsExhaustive: () => preferMatchTagsExhaustive,
|
|
59
|
+
preferPredicateIsTagged: () => preferPredicateIsTagged
|
|
54
60
|
});
|
|
55
61
|
//#endregion
|
|
56
62
|
export { rules_exports };
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { isEffectCall, tagComparisonsInOr } from "./_tagged-control-flow.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/rules/prefer-catch-tag.ts
|
|
8
|
+
const returnedExpression = (fn) => {
|
|
9
|
+
if (fn.body === null) return null;
|
|
10
|
+
if (fn.body.type !== "BlockStatement") return fn.body;
|
|
11
|
+
if (fn.body.body.length !== 1) return null;
|
|
12
|
+
const statement = fn.body.body[0];
|
|
13
|
+
if (statement?.type !== "ReturnStatement" || statement.argument === null) return null;
|
|
14
|
+
return statement.argument;
|
|
15
|
+
};
|
|
16
|
+
const hasManualTagPredicate = (argument) => {
|
|
17
|
+
if (argument?.type !== "ArrowFunctionExpression" && argument?.type !== "FunctionExpression") return false;
|
|
18
|
+
const parameter = argument.params[0];
|
|
19
|
+
if (parameter?.type !== "Identifier") return false;
|
|
20
|
+
const expression = returnedExpression(argument);
|
|
21
|
+
if (expression === null) return false;
|
|
22
|
+
const comparisons = tagComparisonsInOr(expression);
|
|
23
|
+
if (comparisons === null || comparisons.length === 0) return false;
|
|
24
|
+
return comparisons.every((comparison) => comparison.subject === parameter.name);
|
|
25
|
+
};
|
|
26
|
+
const preferCatchTag = define({
|
|
27
|
+
name: "prefer-catch-tag",
|
|
28
|
+
meta: meta({
|
|
29
|
+
type: "suggestion",
|
|
30
|
+
description: "Use Effect.catchTag or Effect.catchTags for tagged failures."
|
|
31
|
+
}),
|
|
32
|
+
create: function* () {
|
|
33
|
+
const context = yield* RuleContext;
|
|
34
|
+
return { CallExpression: (node) => {
|
|
35
|
+
if (!isEffectCall(node, "catchIf")) return Effect.void;
|
|
36
|
+
let predicateIndex = 1;
|
|
37
|
+
if (node.arguments.length === 2) predicateIndex = 0;
|
|
38
|
+
const predicate = node.arguments[predicateIndex];
|
|
39
|
+
if (!hasManualTagPredicate(predicate)) return Effect.void;
|
|
40
|
+
return context.report(make({
|
|
41
|
+
node,
|
|
42
|
+
message: "Use Effect.catchTag for one tagged failure or Effect.catchTags for multiple tagged failures."
|
|
43
|
+
}));
|
|
44
|
+
} };
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
export { preferCatchTag };
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
|
|
4
5
|
import "../vendor/effect-oxlint/index.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
5
7
|
import * as Effect from "effect/Effect";
|
|
6
8
|
//#region src/rules/prefer-effect-fn.ts
|
|
7
|
-
const isStaticMember = (node,
|
|
8
|
-
const isEffectGenCall = (node) => node.type === "CallExpression" && node.callee.type !== "Super" && isStaticMember(node.callee,
|
|
9
|
-
const isEffectWithSpanCall = (node) => node.type === "CallExpression" && node.callee.type !== "Super" && isStaticMember(node.callee,
|
|
10
|
-
const spansEffectGenDirectly = (node) => node.callee.type !== "Super" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && node.callee.property.name === "withSpan" && isEffectGenCall(node.callee.object);
|
|
11
|
-
const
|
|
9
|
+
const isStaticMember = (node, objects, property) => node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && objects.has(node.object.name) && node.property.type === "Identifier" && node.property.name === property;
|
|
10
|
+
const isEffectGenCall = (node, effectNamespaces) => node.type === "CallExpression" && node.callee.type !== "Super" && isStaticMember(node.callee, effectNamespaces, "gen");
|
|
11
|
+
const isEffectWithSpanCall = (node, effectNamespaces) => node.type === "CallExpression" && node.callee.type !== "Super" && isStaticMember(node.callee, effectNamespaces, "withSpan");
|
|
12
|
+
const spansEffectGenDirectly = (node, effectNamespaces) => node.callee.type !== "Super" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && node.callee.property.name === "withSpan" && isEffectGenCall(node.callee.object, effectNamespaces);
|
|
13
|
+
const pipesWithSpanFromEffectGen = (node, effectNamespaces) => node.callee.type !== "Super" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && node.callee.property.name === "pipe" && isEffectGenCall(node.callee.object, effectNamespaces) && node.arguments.some((argument) => isEffectWithSpanCall(argument, effectNamespaces));
|
|
14
|
+
const effectNamespaceFromImport = (node) => {
|
|
15
|
+
const source = node.source.value;
|
|
16
|
+
if (source !== "effect" && source !== "effect/Effect") return [];
|
|
17
|
+
const namespaces = [];
|
|
18
|
+
for (const specifier of node.specifiers) {
|
|
19
|
+
if (specifier.type === "ImportNamespaceSpecifier" && source === "effect/Effect") {
|
|
20
|
+
namespaces.push(specifier.local.name);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (specifier.type === "ImportSpecifier" && source === "effect" && specifier.imported.type === "Identifier" && specifier.imported.name === "Effect") namespaces.push(specifier.local.name);
|
|
24
|
+
}
|
|
25
|
+
return namespaces;
|
|
26
|
+
};
|
|
12
27
|
const preferEffectFn = define({
|
|
13
28
|
name: "prefer-effect-fn",
|
|
14
29
|
meta: meta({
|
|
@@ -17,14 +32,27 @@ const preferEffectFn = define({
|
|
|
17
32
|
}),
|
|
18
33
|
create: function* () {
|
|
19
34
|
const ctx = yield* RuleContext;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
const effectNamespaces = /* @__PURE__ */ new Set();
|
|
36
|
+
const visibleEffectNamespaces = (node) => new Set([...effectNamespaces].filter((namespace) => Option.match(findVariableUp(ctx.sourceCode.getScope(node), namespace), {
|
|
37
|
+
onNone: () => true,
|
|
38
|
+
onSome: (variable) => variable.defs.some((definition) => definition.type === "ImportBinding")
|
|
39
|
+
})));
|
|
40
|
+
return {
|
|
41
|
+
ImportDeclaration: (node) => {
|
|
42
|
+
if (node.type !== "ImportDeclaration") return Effect.void;
|
|
43
|
+
for (const namespace of effectNamespaceFromImport(node)) effectNamespaces.add(namespace);
|
|
44
|
+
return Effect.void;
|
|
45
|
+
},
|
|
46
|
+
CallExpression: (node) => {
|
|
47
|
+
if (node.type !== "CallExpression") return Effect.void;
|
|
48
|
+
const namespaces = visibleEffectNamespaces(node);
|
|
49
|
+
if (!spansEffectGenDirectly(node, namespaces) && !pipesWithSpanFromEffectGen(node, namespaces)) return Effect.void;
|
|
50
|
+
return ctx.report(make({
|
|
51
|
+
node,
|
|
52
|
+
message: "Avoid a span on Effect.gen. Define the named operation with Effect.fn."
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
28
56
|
}
|
|
29
57
|
});
|
|
30
58
|
//#endregion
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { isInsideCatchAllHandler, taggedSwitchSubject } from "./_tagged-control-flow.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/rules/prefer-match-tags-exhaustive.ts
|
|
8
|
+
const statementReturns = (statement) => {
|
|
9
|
+
if (statement.type === "ReturnStatement") return true;
|
|
10
|
+
if (statement.type !== "BlockStatement") return false;
|
|
11
|
+
const last = statement.body.at(-1);
|
|
12
|
+
if (last === void 0) return false;
|
|
13
|
+
return statementReturns(last);
|
|
14
|
+
};
|
|
15
|
+
const caseReturns = (switchCase) => {
|
|
16
|
+
const last = switchCase.consequent.at(-1);
|
|
17
|
+
if (last === void 0) return false;
|
|
18
|
+
return statementReturns(last);
|
|
19
|
+
};
|
|
20
|
+
const isCompleteTransformationShape = (node) => {
|
|
21
|
+
if (node.cases.length < 2 || taggedSwitchSubject(node) === null) return false;
|
|
22
|
+
for (const switchCase of node.cases) {
|
|
23
|
+
if (switchCase.test === null || switchCase.test.type !== "Literal") return false;
|
|
24
|
+
if (typeof switchCase.test.value !== "string" || !caseReturns(switchCase)) return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
const preferMatchTagsExhaustive = define({
|
|
29
|
+
name: "prefer-match-tags-exhaustive",
|
|
30
|
+
meta: meta({
|
|
31
|
+
type: "suggestion",
|
|
32
|
+
description: "Use Match.tagsExhaustive for a complete tagged-union transformation."
|
|
33
|
+
}),
|
|
34
|
+
create: function* () {
|
|
35
|
+
const context = yield* RuleContext;
|
|
36
|
+
return { SwitchStatement: (node) => {
|
|
37
|
+
if (!isCompleteTransformationShape(node) || isInsideCatchAllHandler(node)) return Effect.void;
|
|
38
|
+
return context.report(make({
|
|
39
|
+
node,
|
|
40
|
+
message: "Use Match.type with Match.tagsExhaustive so a new tagged variant requires a new case."
|
|
41
|
+
}));
|
|
42
|
+
} };
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
//#endregion
|
|
46
|
+
export { preferMatchTagsExhaustive };
|
|
@@ -0,0 +1,37 @@
|
|
|
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 { effectCallbackLocation, hasOneTaggedSubject, tagComparisonsInOr } from "./_tagged-control-flow.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/rules/prefer-predicate-is-tagged.ts
|
|
8
|
+
const isCatchIfPredicate = (node) => {
|
|
9
|
+
const location = effectCallbackLocation(node, "catchIf");
|
|
10
|
+
if (location === null) return false;
|
|
11
|
+
let predicateIndex = 1;
|
|
12
|
+
if (location.argumentCount === 2) predicateIndex = 0;
|
|
13
|
+
return location.index === predicateIndex;
|
|
14
|
+
};
|
|
15
|
+
const preferPredicateIsTagged = define({
|
|
16
|
+
name: "prefer-predicate-is-tagged",
|
|
17
|
+
meta: meta({
|
|
18
|
+
type: "suggestion",
|
|
19
|
+
description: "Use Predicate.isTagged for combined tagged-value narrowing."
|
|
20
|
+
}),
|
|
21
|
+
create: function* () {
|
|
22
|
+
const context = yield* RuleContext;
|
|
23
|
+
return { LogicalExpression: (node) => {
|
|
24
|
+
if (node.operator !== "||") return Effect.void;
|
|
25
|
+
if (node.parent?.type === "LogicalExpression" && node.parent.operator === "||") return Effect.void;
|
|
26
|
+
if (isCatchIfPredicate(node)) return Effect.void;
|
|
27
|
+
const comparisons = tagComparisonsInOr(node);
|
|
28
|
+
if (comparisons === null || !hasOneTaggedSubject(comparisons)) return Effect.void;
|
|
29
|
+
return context.report(make({
|
|
30
|
+
node,
|
|
31
|
+
message: "Use a named Predicate refinement composed from Predicate.isTagged for multiple tags."
|
|
32
|
+
}));
|
|
33
|
+
} };
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
//#endregion
|
|
37
|
+
export { preferPredicateIsTagged };
|