oxlint-plugin-effect 0.8.2 → 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 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 | 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/preferEffectFn` | Requires `Effect.fn` for a generator operation that adds a span |
43
- | `effect/noGlobals` | Bans ambient capabilities with direct Effect replacements; allows `process.std*.isTTY` |
44
- | `effect/noNodeBuiltinImport` | Bans fully replaced Node modules and replaced operations from partial modules |
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`, ordinary `if` and `switch` statements, and runtime runners at explicit application boundaries.
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/preferEffectFn": "error"
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 };
@@ -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 };
@@ -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
- preferEffectFn: () => preferEffectFn
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,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/prefer-catch-tag.d.ts
4
+ declare const preferCatchTag: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { preferCatchTag };
@@ -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 };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/prefer-match-tags-exhaustive.d.ts
4
+ declare const preferMatchTagsExhaustive: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { preferMatchTagsExhaustive };
@@ -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,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/prefer-predicate-is-tagged.d.ts
4
+ declare const preferPredicateIsTagged: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { preferPredicateIsTagged };
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-effect",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "oxlint plugin with Effect-specific lint rules",
5
5
  "license": "MIT",
6
6
  "repository": {