mirror of
https://github.com/dmmulroy/anti-slop.git
synced 2026-09-14 19:08:27 +08:00
feat(effect): prefer Match for literal branches
This commit is contained in:
committed by
Dillon Mulroy
parent
01b8f46979
commit
e6676e8d0b
@@ -95,7 +95,11 @@ export default defineConfig({
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
"anti-slop-effect/no-service-constructor-imports": "error"
|
||||
"anti-slop-effect/no-manual-effect-error-tag": "error",
|
||||
"anti-slop-effect/no-manual-tag-comparison": "error",
|
||||
"anti-slop-effect/no-manual-tagged-construction": "error",
|
||||
"anti-slop-effect/no-service-constructor-imports": "error",
|
||||
"anti-slop-effect/prefer-effect-match": "error"
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -125,7 +129,11 @@ export default defineConfig({
|
||||
|
||||
### Effect rules
|
||||
|
||||
- `no-manual-effect-error-tag` — rejects manual `_tag` comparisons and switches inside broad `Effect.catch`, `Effect.catchAll`, and `Effect.catchIf` handlers in favor of tagged error handlers.
|
||||
- `no-manual-tag-comparison` — rejects direct `_tag` comparisons and `_tag` switches in favor of `Match`, `Predicate.isTagged`, or tagged-enum matching.
|
||||
- `no-manual-tagged-construction` — rejects literal `_tag` object construction in favor of Schema, tagged class/error, or `Data.taggedEnum` constructors. `Match.when` and `Match.not` patterns remain allowed.
|
||||
- `no-service-constructor-imports` — rejects named `make<CapabilityName>` imports from relative project modules outside `*.test.*` and `*.spec.*` files. Runtime callers should import the owning Layer and yield the contextual service instead. Package and path-alias imports, default imports, and static constructors such as `WorkspaceName.make` are outside the rule.
|
||||
- `prefer-effect-match` — rejects chained literal ternaries over the same value in favor of Effect's `Match` API.
|
||||
|
||||
### Analysis boundaries
|
||||
|
||||
@@ -284,6 +292,22 @@ import { makeIssueService } from "./issue-service.ts";
|
||||
|
||||
Import the owning Layer and yield `IssueService` instead. Focused `*.test.*` and `*.spec.*` files may import the constructor directly.
|
||||
|
||||
### Effect: tagged values and matching
|
||||
|
||||
```ts
|
||||
if (result._tag === "Ready") useReady(result);
|
||||
|
||||
const result = { _tag: "Ready", value };
|
||||
|
||||
Effect.catch((error) =>
|
||||
error._tag === "NotFound" ? recover : Effect.fail(error)
|
||||
);
|
||||
|
||||
const label = kind === "a" ? "A" : kind === "b" ? "B" : "Other";
|
||||
```
|
||||
|
||||
Use `Predicate.isTagged` for reusable predicates, `Match` or tagged-enum matching for branching, tagged constructors for values, and `Effect.catchTag`/`Effect.catchTags` for tagged errors.
|
||||
|
||||
### `no-unknown-parameters`
|
||||
|
||||
```ts
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"scripts": {
|
||||
"check": "pnpm lint && pnpm test && pnpm typecheck && pnpm check:skill-assets",
|
||||
"lint": "oxlint src",
|
||||
"test": "tsx src/rules/require-readable-spacing.test.ts && tsx src/rules/require-readable-spacing-cli.test.ts && tsx src/rules/no-array-filter-map.test.ts && tsx src/rules/no-reduce-accumulator-copy.test.ts && tsx src/rules/no-chained-type-assertions.test.ts && tsx src/rules/no-conditional-empty-object-spread.test.ts && tsx src/rules/no-unsafe-dictionary-type.test.ts && tsx src/rules/no-known-value-widening.test.ts && tsx src/rules/no-object-parameters.test.ts && tsx src/rules/no-unknown-parameters.test.ts && tsx src/rules/no-runtime-typeof.test.ts && tsx src/rules/no-shape-in-symbol-names.test.ts && tsx src/rules/no-unknown-returns.test.ts && tsx src/rules/no-unknown-type-aliases.test.ts && tsx src/rules/no-widen-then-assert.test.ts && tsx src/rules/no-reflect-get.test.ts && tsx src/rules/no-reflect-apply.test.ts && tsx src/rules/no-module-mocking.test.ts && tsx src/rules/require-safety-comment-for-type-assertion.test.ts && tsx src/effect/rules/no-service-constructor-imports.test.ts && tsx src/effect/rules/no-manual-tag-comparison.test.ts && tsx src/effect/rules/no-manual-effect-error-tag.test.ts && tsx src/effect/rules/no-manual-tagged-construction.test.ts",
|
||||
"test": "tsx src/rules/require-readable-spacing.test.ts && tsx src/rules/require-readable-spacing-cli.test.ts && tsx src/rules/no-array-filter-map.test.ts && tsx src/rules/no-reduce-accumulator-copy.test.ts && tsx src/rules/no-chained-type-assertions.test.ts && tsx src/rules/no-conditional-empty-object-spread.test.ts && tsx src/rules/no-unsafe-dictionary-type.test.ts && tsx src/rules/no-known-value-widening.test.ts && tsx src/rules/no-object-parameters.test.ts && tsx src/rules/no-unknown-parameters.test.ts && tsx src/rules/no-runtime-typeof.test.ts && tsx src/rules/no-shape-in-symbol-names.test.ts && tsx src/rules/no-unknown-returns.test.ts && tsx src/rules/no-unknown-type-aliases.test.ts && tsx src/rules/no-widen-then-assert.test.ts && tsx src/rules/no-reflect-get.test.ts && tsx src/rules/no-reflect-apply.test.ts && tsx src/rules/no-module-mocking.test.ts && tsx src/rules/require-safety-comment-for-type-assertion.test.ts && tsx src/effect/rules/no-service-constructor-imports.test.ts && tsx src/effect/rules/no-manual-tag-comparison.test.ts && tsx src/effect/rules/no-manual-effect-error-tag.test.ts && tsx src/effect/rules/no-manual-tagged-construction.test.ts && tsx src/effect/rules/prefer-effect-match.test.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"sync:skill-assets": "node scripts/sync-skill-assets.mjs",
|
||||
"check:skill-assets": "node scripts/sync-skill-assets.mjs --check"
|
||||
|
||||
@@ -104,7 +104,11 @@ Complete when the operation and target path are established and pre-existing wor
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
"anti-slop-effect/no-manual-effect-error-tag": "error",
|
||||
"anti-slop-effect/no-manual-tag-comparison": "error",
|
||||
"anti-slop-effect/no-manual-tagged-construction": "error",
|
||||
"anti-slop-effect/no-service-constructor-imports": "error",
|
||||
"anti-slop-effect/prefer-effect-match": "error",
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { noManualEffectErrorTagRule } from "./rules/no-manual-effect-error-tag.t
|
||||
import { noManualTagComparisonRule } from "./rules/no-manual-tag-comparison.ts";
|
||||
import { noManualTaggedConstructionRule } from "./rules/no-manual-tagged-construction.ts";
|
||||
import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts";
|
||||
import { preferEffectMatchRule } from "./rules/prefer-effect-match.ts";
|
||||
|
||||
/** Opt-in Oxlint rules for Effect service and Layer architecture. */
|
||||
const antiSlopEffectPlugin = eslintCompatPlugin({
|
||||
@@ -13,6 +14,7 @@ const antiSlopEffectPlugin = eslintCompatPlugin({
|
||||
"no-manual-tag-comparison": noManualTagComparisonRule,
|
||||
"no-manual-tagged-construction": noManualTaggedConstructionRule,
|
||||
"no-service-constructor-imports": noServiceConstructorImportsRule,
|
||||
"prefer-effect-match": preferEffectMatchRule,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { defineRule, type ESTree } from "@oxlint/plugins";
|
||||
|
||||
const equalityOperators = new Set(["==", "===", "!=", "!=="]);
|
||||
|
||||
export const preferEffectMatchRule = defineRule({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Use Match from Effect for chained literal ternaries over the same value.",
|
||||
},
|
||||
messages: {
|
||||
preferMatch:
|
||||
"Use Match from Effect instead of a chained literal ternary.",
|
||||
},
|
||||
},
|
||||
createOnce(context) {
|
||||
const isLiteral = (node: ESTree.Node): boolean =>
|
||||
node.type === "Literal" ||
|
||||
(node.type === "TemplateLiteral" && node.expressions.length === 0);
|
||||
|
||||
const comparedValue = (node: ESTree.Expression): string | undefined => {
|
||||
if (
|
||||
node.type !== "BinaryExpression" ||
|
||||
!equalityOperators.has(node.operator)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (isLiteral(node.left)) return context.sourceCode.getText(node.right);
|
||||
if (isLiteral(node.right)) return context.sourceCode.getText(node.left);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
ConditionalExpression(node) {
|
||||
if (node.parent?.type === "ConditionalExpression") return;
|
||||
const value = comparedValue(node.test);
|
||||
if (value === undefined) return;
|
||||
|
||||
let alternate = node.alternate;
|
||||
let literalChecks = 1;
|
||||
while (alternate.type === "ConditionalExpression") {
|
||||
if (comparedValue(alternate.test) !== value) return;
|
||||
literalChecks += 1;
|
||||
alternate = alternate.alternate;
|
||||
}
|
||||
|
||||
if (literalChecks > 1) {
|
||||
context.report({ node, messageId: "preferMatch" });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { noManualEffectErrorTagRule } from "./rules/no-manual-effect-error-tag.t
|
||||
import { noManualTagComparisonRule } from "./rules/no-manual-tag-comparison.ts";
|
||||
import { noManualTaggedConstructionRule } from "./rules/no-manual-tagged-construction.ts";
|
||||
import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts";
|
||||
import { preferEffectMatchRule } from "./rules/prefer-effect-match.ts";
|
||||
|
||||
/** Opt-in Oxlint rules for Effect service and Layer architecture. */
|
||||
const antiSlopEffectPlugin = eslintCompatPlugin({
|
||||
@@ -13,6 +14,7 @@ const antiSlopEffectPlugin = eslintCompatPlugin({
|
||||
"no-manual-tag-comparison": noManualTagComparisonRule,
|
||||
"no-manual-tagged-construction": noManualTaggedConstructionRule,
|
||||
"no-service-constructor-imports": noServiceConstructorImportsRule,
|
||||
"prefer-effect-match": preferEffectMatchRule,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { RuleTester } from "oxlint/plugins-dev";
|
||||
|
||||
import { preferEffectMatchRule } from "./prefer-effect-match.ts";
|
||||
|
||||
new RuleTester().run("prefer-effect-match", preferEffectMatchRule, {
|
||||
valid: [
|
||||
'kind === "a" ? first : fallback;',
|
||||
'kind === "a" ? first : other === "b" ? second : fallback;',
|
||||
"condition ? first : otherCondition ? second : fallback;",
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'kind === "a" ? first : kind === "b" ? second : fallback;',
|
||||
errors: [{ messageId: "preferMatch" }],
|
||||
},
|
||||
{
|
||||
code: '`a` !== kind ? first : `b` === kind ? second : fallback;',
|
||||
errors: [{ messageId: "preferMatch" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { defineRule, type ESTree } from "@oxlint/plugins";
|
||||
|
||||
const equalityOperators = new Set(["==", "===", "!=", "!=="]);
|
||||
|
||||
export const preferEffectMatchRule = defineRule({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Use Match from Effect for chained literal ternaries over the same value.",
|
||||
},
|
||||
messages: {
|
||||
preferMatch:
|
||||
"Use Match from Effect instead of a chained literal ternary.",
|
||||
},
|
||||
},
|
||||
createOnce(context) {
|
||||
const isLiteral = (node: ESTree.Node): boolean =>
|
||||
node.type === "Literal" ||
|
||||
(node.type === "TemplateLiteral" && node.expressions.length === 0);
|
||||
|
||||
const comparedValue = (node: ESTree.Expression): string | undefined => {
|
||||
if (
|
||||
node.type !== "BinaryExpression" ||
|
||||
!equalityOperators.has(node.operator)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (isLiteral(node.left)) return context.sourceCode.getText(node.right);
|
||||
if (isLiteral(node.right)) return context.sourceCode.getText(node.left);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
ConditionalExpression(node) {
|
||||
if (node.parent?.type === "ConditionalExpression") return;
|
||||
const value = comparedValue(node.test);
|
||||
if (value === undefined) return;
|
||||
|
||||
let alternate = node.alternate;
|
||||
let literalChecks = 1;
|
||||
while (alternate.type === "ConditionalExpression") {
|
||||
if (comparedValue(alternate.test) !== value) return;
|
||||
literalChecks += 1;
|
||||
alternate = alternate.alternate;
|
||||
}
|
||||
|
||||
if (literalChecks > 1) {
|
||||
context.report({ node, messageId: "preferMatch" });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user