@doubleloop-io/oxlint-plugin-effect 0.1.0-rc.1 → 0.1.0-rc.2
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 +74 -56
- package/RULES.md +520 -395
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -4
- package/dist/presets/all.json +2 -2
- package/dist/rules/adapter-import-boundary.js +5 -12
- package/dist/rules/all-errors-handled-at-boundary.js +3 -9
- package/dist/rules/anti-slop-no-service-constructor-imports.js +1 -8
- package/dist/rules/exported-schema-identifier.js +15 -10
- package/dist/rules/no-effect-promise.js +1 -8
- package/dist/rules/no-manual-discriminant-comparison.js +3 -9
- package/dist/rules/no-manual-discriminant-construction.js +3 -9
- package/dist/rules/no-provide-on-definition.js +1 -8
- package/dist/rules/no-throw-or-primitive-failure.d.ts +1 -0
- package/dist/rules/{no-bare-error-in-effect.js → no-throw-or-primitive-failure.js} +2 -9
- package/dist/rules/no-yielded-match.js +9 -2
- package/dist/rules/prefer-effect-vitest.js +1 -8
- package/dist/rules/provide-named-layer.js +1 -8
- package/dist/rules/redacted-secret-names.js +18 -5
- package/dist/rules/run-only-in-runner.js +1 -8
- package/dist/rules/service-key-matches-name.js +15 -6
- package/dist/rules/service-port-shape-only.js +10 -18
- package/dist/rules/single-style-per-function.js +1 -8
- package/dist/rules/split-success-path-from-error-handling.d.ts +1 -0
- package/dist/rules/{error-handling-in-effect-fn-handler.js → split-success-path-from-error-handling.js} +2 -9
- package/dist/shared/entry-files.d.ts +1 -1
- package/dist/shared/file-globs.d.ts +3 -9
- package/dist/shared/file-globs.js +3 -12
- package/dist/shared/settings.d.ts +5 -0
- package/dist/shared/settings.js +26 -0
- package/dist/shared/tagged-values.d.ts +2 -8
- package/dist/shared/tagged-values.js +3 -14
- package/dist/shared/test-files.d.ts +1 -1
- package/package.json +5 -4
- package/dist/rules/error-handling-in-effect-fn-handler.d.ts +0 -1
- package/dist/rules/no-bare-error-in-effect.d.ts +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { defaultEntryFiles } from "./shared/entry-files.ts";
|
|
2
|
+
export { defaultDiscriminants } from "./shared/tagged-values.ts";
|
|
2
3
|
export { defaultTestFiles } from "./shared/test-files.ts";
|
|
3
4
|
export declare const pluginName = "dl-effect";
|
|
4
5
|
export declare const pluginPath: string;
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,8 @@ import { allErrorsHandledAtBoundaryRule } from "./rules/all-errors-handled-at-bo
|
|
|
5
5
|
import { antiSlopNoManualEffectErrorTagRule } from "./rules/anti-slop-no-manual-effect-error-tag.js";
|
|
6
6
|
import { antiSlopNoServiceConstructorImportsRule } from "./rules/anti-slop-no-service-constructor-imports.js";
|
|
7
7
|
import { antiSlopPreferEffectMatchRule } from "./rules/anti-slop-prefer-effect-match.js";
|
|
8
|
-
import { errorHandlingInEffectFnHandlerRule } from "./rules/error-handling-in-effect-fn-handler.js";
|
|
9
8
|
import { exportedSchemaIdentifierRule } from "./rules/exported-schema-identifier.js";
|
|
10
9
|
import { noAsymmetricMatchHandlersRule } from "./rules/no-asymmetric-match-handlers.js";
|
|
11
|
-
import { noBareErrorInEffectRule } from "./rules/no-bare-error-in-effect.js";
|
|
12
10
|
import { noEffectPromiseRule } from "./rules/no-effect-promise.js";
|
|
13
11
|
import { noEffectReturnTypeRule } from "./rules/no-effect-return-type.js";
|
|
14
12
|
import { noInlineSchemaCodecRule } from "./rules/no-inline-schema-codec.js";
|
|
@@ -18,6 +16,7 @@ import { noManualDiscriminantConstructionRule } from "./rules/no-manual-discrimi
|
|
|
18
16
|
import { noNestedMatchRule } from "./rules/no-nested-match.js";
|
|
19
17
|
import { noParameterlessCatchAllRule } from "./rules/no-parameterless-catch-all.js";
|
|
20
18
|
import { noProvideOnDefinitionRule } from "./rules/no-provide-on-definition.js";
|
|
19
|
+
import { noThrowOrPrimitiveFailureRule } from "./rules/no-throw-or-primitive-failure.js";
|
|
21
20
|
import { noTransportTypesInGeneratorRule } from "./rules/no-transport-types-in-generator.js";
|
|
22
21
|
import { noYieldedMatchRule } from "./rules/no-yielded-match.js";
|
|
23
22
|
import { preferEffectFnRule } from "./rules/prefer-effect-fn.js";
|
|
@@ -29,9 +28,11 @@ import { serviceBuiltWithOfRule } from "./rules/service-built-with-of.js";
|
|
|
29
28
|
import { serviceKeyMatchesNameRule } from "./rules/service-key-matches-name.js";
|
|
30
29
|
import { servicePortShapeOnlyRule } from "./rules/service-port-shape-only.js";
|
|
31
30
|
import { singleStylePerFunctionRule } from "./rules/single-style-per-function.js";
|
|
31
|
+
import { splitSuccessPathFromErrorHandlingRule } from "./rules/split-success-path-from-error-handling.js";
|
|
32
32
|
import { tryObjectFormRule } from "./rules/try-object-form.js";
|
|
33
33
|
import { untracedRecursionRule } from "./rules/untraced-recursion.js";
|
|
34
34
|
export { defaultEntryFiles } from "./shared/entry-files.js";
|
|
35
|
+
export { defaultDiscriminants } from "./shared/tagged-values.js";
|
|
35
36
|
export { defaultTestFiles } from "./shared/test-files.js";
|
|
36
37
|
export const pluginName = "dl-effect";
|
|
37
38
|
// Resolved at runtime so it points at this file both from the sources and from `dist/`.
|
|
@@ -44,10 +45,10 @@ const dlEffectPlugin = eslintCompatPlugin({
|
|
|
44
45
|
"anti-slop-prefer-effect-match": antiSlopPreferEffectMatchRule,
|
|
45
46
|
"adapter-import-boundary": adapterImportBoundaryRule,
|
|
46
47
|
"all-errors-handled-at-boundary": allErrorsHandledAtBoundaryRule,
|
|
47
|
-
"
|
|
48
|
+
"split-success-path-from-error-handling": splitSuccessPathFromErrorHandlingRule,
|
|
48
49
|
"exported-schema-identifier": exportedSchemaIdentifierRule,
|
|
49
50
|
"no-asymmetric-match-handlers": noAsymmetricMatchHandlersRule,
|
|
50
|
-
"no-
|
|
51
|
+
"no-throw-or-primitive-failure": noThrowOrPrimitiveFailureRule,
|
|
51
52
|
"no-effect-promise": noEffectPromiseRule,
|
|
52
53
|
"no-effect-return-type": noEffectReturnTypeRule,
|
|
53
54
|
"no-inline-schema-codec": noInlineSchemaCodecRule,
|
package/dist/presets/all.json
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"dl-effect/anti-slop-prefer-effect-match": "warn",
|
|
9
9
|
"dl-effect/adapter-import-boundary": "warn",
|
|
10
10
|
"dl-effect/all-errors-handled-at-boundary": "warn",
|
|
11
|
-
"dl-effect/
|
|
11
|
+
"dl-effect/split-success-path-from-error-handling": "warn",
|
|
12
12
|
"dl-effect/exported-schema-identifier": "warn",
|
|
13
13
|
"dl-effect/no-asymmetric-match-handlers": "warn",
|
|
14
|
-
"dl-effect/no-
|
|
14
|
+
"dl-effect/no-throw-or-primitive-failure": "warn",
|
|
15
15
|
"dl-effect/no-effect-promise": "warn",
|
|
16
16
|
"dl-effect/no-effect-return-type": "warn",
|
|
17
17
|
"dl-effect/no-inline-schema-codec": "warn",
|
|
@@ -2,7 +2,6 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { defineRule } from "@oxlint/plugins";
|
|
4
4
|
import { isEntryFile } from "../shared/entry-files.js";
|
|
5
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
6
5
|
import { isTestFile } from "../shared/test-files.js";
|
|
7
6
|
// Only the APIs that build a service implementation, so composition of existing layers
|
|
8
7
|
// (`mergeAll`, `provide`, `merge`) leaves the entry point out. Longest names first: the
|
|
@@ -28,7 +27,10 @@ const LAYER_CONSTRUCTORS = [
|
|
|
28
27
|
];
|
|
29
28
|
// Like the other rules, names are matched literally: `import * as L from "effect/Layer"` escapes.
|
|
30
29
|
const LAYER_CONSTRUCTION = new RegExp(`\\bLayer\\.(?:${LAYER_CONSTRUCTORS.join("|")})\\s*\\(`, "u");
|
|
31
|
-
|
|
30
|
+
// An Effect 3 `Effect.Service` always carries its implementation and the `Default` layer built
|
|
31
|
+
// from it, so a file that declares one is an adapter even without a `Layer.*` call.
|
|
32
|
+
const EFFECT_SERVICE = /\bEffect\.Service\s*[<(]/u;
|
|
33
|
+
export const buildsLayer = (source) => LAYER_CONSTRUCTION.test(source) || EFFECT_SERVICE.test(source);
|
|
32
34
|
// Test files are skipped so a fixture layer cannot promote a domain directory to an adapter.
|
|
33
35
|
const isSource = (entry) => entry.endsWith(".ts") && !entry.endsWith(".test.ts");
|
|
34
36
|
const buildsLayerIn = (dir) => {
|
|
@@ -64,16 +66,7 @@ export const makeAdapterImportBoundaryRule = (deps) => defineRule({
|
|
|
64
66
|
adapterOutsideEntry: "`{{adapter}}` is an adapter: depend on its port instead and let the entry point provide the Layer.",
|
|
65
67
|
crossAdapter: "`{{adapter}}` is another adapter: depend on its port instead and let the entry point compose the Layers.",
|
|
66
68
|
},
|
|
67
|
-
schema: [
|
|
68
|
-
{
|
|
69
|
-
type: "object",
|
|
70
|
-
properties: {
|
|
71
|
-
entryFiles: filePatternsSchema,
|
|
72
|
-
testFiles: filePatternsSchema,
|
|
73
|
-
},
|
|
74
|
-
additionalProperties: false,
|
|
75
|
-
},
|
|
76
|
-
],
|
|
69
|
+
schema: [],
|
|
77
70
|
},
|
|
78
71
|
create(context) {
|
|
79
72
|
const ownDir = dirname(context.filename);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf, isServerComponent, memberOf, nearestFunction, } from "../shared/effect-ast.js";
|
|
3
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
4
3
|
import { isTestFile } from "../shared/test-files.js";
|
|
5
4
|
// The *Exit runners hand failures back as values, so an open error channel cannot leak there.
|
|
6
5
|
// `NodeRuntime.runMain`/`BunRuntime.runMain` are boundaries too: they log the Cause and exit with
|
|
@@ -26,7 +25,8 @@ const parameterType = (identifier) => {
|
|
|
26
25
|
return undefined;
|
|
27
26
|
};
|
|
28
27
|
// They turn every failure into a success value, so whatever comes before cannot leak.
|
|
29
|
-
|
|
28
|
+
// `either` is the Effect 3 name of `result`.
|
|
29
|
+
const failureToValue = new Set(["result", "either", "exit"]);
|
|
30
30
|
const isPipeCall = (node) => node.callee.type === "MemberExpression" &&
|
|
31
31
|
!node.callee.computed &&
|
|
32
32
|
node.callee.property.type === "Identifier" &&
|
|
@@ -55,13 +55,7 @@ export const allErrorsHandledAtBoundaryRule = defineRule({
|
|
|
55
55
|
messages: {
|
|
56
56
|
openErrorChannel: "Close the error channel before running: `program satisfies Effect.Effect<A, never>`, or a runner parameter typed `Effect.Effect<A, never, never>`.",
|
|
57
57
|
},
|
|
58
|
-
schema: [
|
|
59
|
-
{
|
|
60
|
-
type: "object",
|
|
61
|
-
properties: { testFiles: filePatternsSchema },
|
|
62
|
-
additionalProperties: false,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
58
|
+
schema: [],
|
|
65
59
|
},
|
|
66
60
|
createOnce(context) {
|
|
67
61
|
const frames = [];
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
3
2
|
import { isTestFile } from "../shared/test-files.js";
|
|
4
3
|
const serviceConstructorName = /^make[A-Z]/u;
|
|
5
4
|
const isProjectLocalImport = (source) => source.startsWith("./") || source.startsWith("../");
|
|
@@ -13,13 +12,7 @@ export const antiSlopNoServiceConstructorImportsRule = defineRule({
|
|
|
13
12
|
messages: {
|
|
14
13
|
serviceConstructorImport: 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.',
|
|
15
14
|
},
|
|
16
|
-
schema: [
|
|
17
|
-
{
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: { testFiles: filePatternsSchema },
|
|
20
|
-
additionalProperties: false,
|
|
21
|
-
},
|
|
22
|
-
],
|
|
15
|
+
schema: [],
|
|
23
16
|
},
|
|
24
17
|
createOnce(context) {
|
|
25
18
|
let inTestFile = false;
|
|
@@ -8,6 +8,13 @@ const chainedMethod = (node) => node.type === "CallExpression" &&
|
|
|
8
8
|
node.callee.property.type === "Identifier"
|
|
9
9
|
? { name: node.callee.property.name, receiver: node.callee.object, call: node }
|
|
10
10
|
: undefined;
|
|
11
|
+
// `annotations` is the Effect 3 name of `annotate`.
|
|
12
|
+
const isAnnotate = (name) => name === "annotate" || name === "annotations";
|
|
13
|
+
// One suggestion per version instead of detecting the installed one: the fix stays syntax only.
|
|
14
|
+
const annotateMethods = [
|
|
15
|
+
{ version: "v3", method: "annotations" },
|
|
16
|
+
{ version: "v4", method: "annotate" },
|
|
17
|
+
];
|
|
11
18
|
// `X.pipe(Schema.annotate(a))` and `X.annotate(a)`, in any order along the chain.
|
|
12
19
|
const annotationsOf = (init) => {
|
|
13
20
|
const annotations = [];
|
|
@@ -16,11 +23,11 @@ const annotationsOf = (init) => {
|
|
|
16
23
|
const link = chainedMethod(current);
|
|
17
24
|
if (link === undefined)
|
|
18
25
|
return undefined;
|
|
19
|
-
if (link.name
|
|
26
|
+
if (isAnnotate(link.name))
|
|
20
27
|
annotations.push(...link.call.arguments);
|
|
21
28
|
else if (link.name === "pipe") {
|
|
22
29
|
for (const argument of link.call.arguments) {
|
|
23
|
-
if (calleeOf(argument, "Schema")
|
|
30
|
+
if (isAnnotate(calleeOf(argument, "Schema")) &&
|
|
24
31
|
argument.type === "CallExpression") {
|
|
25
32
|
annotations.push(...argument.arguments);
|
|
26
33
|
}
|
|
@@ -58,7 +65,7 @@ export const exportedSchemaIdentifierRule = defineRule({
|
|
|
58
65
|
messages: {
|
|
59
66
|
missingIdentifier: 'Annotate `{{name}}` with `identifier: "{{name}}"`, so errors, JSON Schema and arbitraries name it.',
|
|
60
67
|
wrongIdentifier: 'The `identifier` of `{{name}}` is `{{identifier}}`: use `"{{name}}"`, the name the schema is imported by.',
|
|
61
|
-
addIdentifier: '
|
|
68
|
+
addIdentifier: '{{version}}: append `.{{method}}({ identifier: "{{name}}" })`.',
|
|
62
69
|
renameIdentifier: 'Use `"{{name}}"` as identifier.',
|
|
63
70
|
},
|
|
64
71
|
},
|
|
@@ -77,13 +84,11 @@ export const exportedSchemaIdentifierRule = defineRule({
|
|
|
77
84
|
node: id,
|
|
78
85
|
messageId: "missingIdentifier",
|
|
79
86
|
data: { name },
|
|
80
|
-
suggest:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
+
suggest: annotateMethods.map(({ version, method }) => ({
|
|
88
|
+
messageId: "addIdentifier",
|
|
89
|
+
data: { name, version, method },
|
|
90
|
+
fix: (fixer) => fixer.insertTextAfter(init, `.${method}({ identifier: "${name}" })`),
|
|
91
|
+
})),
|
|
87
92
|
});
|
|
88
93
|
return;
|
|
89
94
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf } from "../shared/effect-ast.js";
|
|
3
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
4
3
|
import { isTestFile } from "../shared/test-files.js";
|
|
5
4
|
// Argument index of the release step, where a promise that cannot reject is the normal case.
|
|
6
5
|
const releaseSlots = new Map([
|
|
@@ -30,13 +29,7 @@ export const noEffectPromiseRule = defineRule({
|
|
|
30
29
|
messages: {
|
|
31
30
|
rejectionAsDefect: "`Effect.promise` turns a rejection into a defect: use `Effect.tryPromise({ try, catch })` unless the promise cannot reject.",
|
|
32
31
|
},
|
|
33
|
-
schema: [
|
|
34
|
-
{
|
|
35
|
-
type: "object",
|
|
36
|
-
properties: { testFiles: filePatternsSchema },
|
|
37
|
-
additionalProperties: false,
|
|
38
|
-
},
|
|
39
|
-
],
|
|
32
|
+
schema: [],
|
|
40
33
|
},
|
|
41
34
|
createOnce(context) {
|
|
42
35
|
let inTestFile = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import { configuredDiscriminants, discriminantOf,
|
|
2
|
+
import { configuredDiscriminants, discriminantOf, isEqualityComparison, isInsideBroadEffectHandler, isStringLiteral, } from "../shared/tagged-values.js";
|
|
3
3
|
export const remedyFor = (discriminant) => discriminant === "_tag"
|
|
4
4
|
? "`Match.tagsExhaustive`, the tagged enum `$match` or an exhaustive ts-pattern match; for a single check, `Predicate.isTagged`"
|
|
5
5
|
: `\`Match.discriminatorsExhaustive("${discriminant}")\` or an exhaustive ts-pattern match`;
|
|
@@ -24,13 +24,7 @@ export const noManualDiscriminantComparisonRule = defineRule({
|
|
|
24
24
|
effectVariantComparison: 'For the `"{{tag}}"` variant of {{owner}}, use {{guard}}; for a union of your own, {{remedy}}.',
|
|
25
25
|
manualSwitch: "A `switch` on `{{discriminant}}` still compiles when a variant is added: use {{remedy}}.",
|
|
26
26
|
},
|
|
27
|
-
schema: [
|
|
28
|
-
{
|
|
29
|
-
type: "object",
|
|
30
|
-
properties: { discriminants: discriminantsSchema },
|
|
31
|
-
additionalProperties: false,
|
|
32
|
-
},
|
|
33
|
-
],
|
|
27
|
+
schema: [],
|
|
34
28
|
},
|
|
35
29
|
createOnce(context) {
|
|
36
30
|
let discriminants = new Set();
|
|
@@ -54,7 +48,7 @@ export const noManualDiscriminantComparisonRule = defineRule({
|
|
|
54
48
|
};
|
|
55
49
|
return {
|
|
56
50
|
before() {
|
|
57
|
-
discriminants = configuredDiscriminants(context.
|
|
51
|
+
discriminants = configuredDiscriminants(context.settings);
|
|
58
52
|
},
|
|
59
53
|
BinaryExpression(node) {
|
|
60
54
|
if (!isEqualityComparison(node))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf, isFunctionNode } from "../shared/effect-ast.js";
|
|
3
|
-
import { configuredDiscriminants,
|
|
3
|
+
import { configuredDiscriminants, isStringLiteral, propertyName } from "../shared/tagged-values.js";
|
|
4
4
|
export const remedyFor = (discriminant) => discriminant === "_tag"
|
|
5
5
|
? "the Schema's tagged `.make`, the tagged class or error constructor, or the `Data.taggedEnum` variant constructor"
|
|
6
6
|
: "the Schema's `.make` or the class constructor";
|
|
@@ -96,19 +96,13 @@ export const noManualDiscriminantConstructionRule = defineRule({
|
|
|
96
96
|
messages: {
|
|
97
97
|
manualConstruction: "An object literal with a literal `{{discriminant}}` skips the variant's constructor: use {{remedy}}.",
|
|
98
98
|
},
|
|
99
|
-
schema: [
|
|
100
|
-
{
|
|
101
|
-
type: "object",
|
|
102
|
-
properties: { discriminants: discriminantsSchema },
|
|
103
|
-
additionalProperties: false,
|
|
104
|
-
},
|
|
105
|
-
],
|
|
99
|
+
schema: [],
|
|
106
100
|
},
|
|
107
101
|
createOnce(context) {
|
|
108
102
|
let discriminants = new Set();
|
|
109
103
|
return {
|
|
110
104
|
before() {
|
|
111
|
-
discriminants = configuredDiscriminants(context.
|
|
105
|
+
discriminants = configuredDiscriminants(context.settings);
|
|
112
106
|
},
|
|
113
107
|
ObjectExpression(node) {
|
|
114
108
|
for (const property of node.properties) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf, isEffectFnApplication } from "../shared/effect-ast.js";
|
|
3
3
|
import { runnerName } from "../shared/effect-runners.js";
|
|
4
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
5
4
|
import { isTestFile } from "../shared/test-files.js";
|
|
6
5
|
const pipeReceiver = (call) => {
|
|
7
6
|
const { callee } = call;
|
|
@@ -92,13 +91,7 @@ export const noProvideOnDefinitionRule = defineRule({
|
|
|
92
91
|
messages: {
|
|
93
92
|
provideOnDefinition: "Define the program without layers and apply `Effect.provide` where it is dispatched, to a named program.",
|
|
94
93
|
},
|
|
95
|
-
schema: [
|
|
96
|
-
{
|
|
97
|
-
type: "object",
|
|
98
|
-
properties: { testFiles: filePatternsSchema },
|
|
99
|
-
additionalProperties: false,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
94
|
+
schema: [],
|
|
102
95
|
},
|
|
103
96
|
createOnce(context) {
|
|
104
97
|
let inTestFile = false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noThrowOrPrimitiveFailureRule: import("@oxlint/plugins").Rule;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf, isEffectGenerator, nearestFunction } from "../shared/effect-ast.js";
|
|
3
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
4
3
|
import { isTestFile } from "../shared/test-files.js";
|
|
5
4
|
// Only the two forms the effecttsgo preset misses: `Effect.fail(new Error(…))` and
|
|
6
5
|
// `extends Error` are already reported by global-error-in-effect-failure and extends-native-error.
|
|
7
|
-
export const
|
|
6
|
+
export const noThrowOrPrimitiveFailureRule = defineRule({
|
|
8
7
|
meta: {
|
|
9
8
|
type: "problem",
|
|
10
9
|
docs: {
|
|
@@ -14,13 +13,7 @@ export const noBareErrorInEffectRule = defineRule({
|
|
|
14
13
|
throwInGenerator: "A `throw` inside an Effect generator becomes a defect: `return yield* new TaggedError(...)` instead.",
|
|
15
14
|
primitiveFailure: "Fail with a tagged error (`Data.TaggedError` or `Schema.TaggedError`), not a primitive value.",
|
|
16
15
|
},
|
|
17
|
-
schema: [
|
|
18
|
-
{
|
|
19
|
-
type: "object",
|
|
20
|
-
properties: { testFiles: filePatternsSchema },
|
|
21
|
-
additionalProperties: false,
|
|
22
|
-
},
|
|
23
|
-
],
|
|
16
|
+
schema: [],
|
|
24
17
|
},
|
|
25
18
|
createOnce(context) {
|
|
26
19
|
let inTestFile = false;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf, isEffectGenerator, isFunctionNode, memberOf, nearestFunction, } from "../shared/effect-ast.js";
|
|
3
3
|
import { handlersOf, nestedBranches } from "../shared/match-ast.js";
|
|
4
|
-
|
|
4
|
+
// `Either` is the Effect 3 name of `Result`.
|
|
5
|
+
const guards = {
|
|
6
|
+
Option: "Option.isNone",
|
|
7
|
+
Result: "Result.isFailure",
|
|
8
|
+
Either: "Either.isLeft",
|
|
9
|
+
};
|
|
5
10
|
const matchedModule = (node) => {
|
|
6
11
|
if (node?.type !== "CallExpression")
|
|
7
12
|
return undefined;
|
|
@@ -9,6 +14,8 @@ const matchedModule = (node) => {
|
|
|
9
14
|
return "Option";
|
|
10
15
|
if (memberOf(node.callee, "Result") === "match")
|
|
11
16
|
return "Result";
|
|
17
|
+
if (memberOf(node.callee, "Either") === "match")
|
|
18
|
+
return "Either";
|
|
12
19
|
return undefined;
|
|
13
20
|
};
|
|
14
21
|
const isPipe = (callee) => (callee.type === "Identifier" && callee.name === "pipe") ||
|
|
@@ -58,7 +65,7 @@ export const noYieldedMatchRule = defineRule({
|
|
|
58
65
|
meta: {
|
|
59
66
|
type: "suggestion",
|
|
60
67
|
docs: {
|
|
61
|
-
description: "Inside an Effect generator, return early on the empty or failed case instead of yielding
|
|
68
|
+
description: "Inside an Effect generator, return early on the empty or failed case instead of yielding a match (`Option.match`; v3: `Either.match`, v4: `Result.match`).",
|
|
62
69
|
},
|
|
63
70
|
messages: {
|
|
64
71
|
yieldedMatch: "This `yield*` of a `{{module}}.match` splits the generator into two branches: return early with `if ({{guard}}(…)) return …` and keep one main path.",
|
|
@@ -2,7 +2,6 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { defineRule } from "@oxlint/plugins";
|
|
4
4
|
import { runnerName } from "../shared/effect-runners.js";
|
|
5
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
6
5
|
import { isTestFile } from "../shared/test-files.js";
|
|
7
6
|
// Without `@effect/vitest` the remedy is out of reach, so the rule stays silent. Not cached:
|
|
8
7
|
// a long-lived linter would miss a later install, and only test files pay for the lookup.
|
|
@@ -24,13 +23,7 @@ export const preferEffectVitestRule = defineRule({
|
|
|
24
23
|
messages: {
|
|
25
24
|
runInTest: "`{{name}}` runs an Effect program in a test: let `@effect/vitest` run it, so the test gets the test runtime and its fixtures become Layers.",
|
|
26
25
|
},
|
|
27
|
-
schema: [
|
|
28
|
-
{
|
|
29
|
-
type: "object",
|
|
30
|
-
properties: { testFiles: filePatternsSchema },
|
|
31
|
-
additionalProperties: false,
|
|
32
|
-
},
|
|
33
|
-
],
|
|
26
|
+
schema: [],
|
|
34
27
|
},
|
|
35
28
|
createOnce(context) {
|
|
36
29
|
let checked = false;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf } from "../shared/effect-ast.js";
|
|
3
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
4
3
|
import { isTestFile } from "../shared/test-files.js";
|
|
5
4
|
const matchers = new Set(["match", "$match"]);
|
|
6
5
|
// Factories and module members (`planLayer(csv)`, `CsvSheetReader.layer(path)`) are names;
|
|
@@ -26,13 +25,7 @@ export const provideNamedLayerRule = defineRule({
|
|
|
26
25
|
messages: {
|
|
27
26
|
inlineLayer: "Name the layer passed to `Effect.provide` (`const appLayer = Layer.mergeAll(...)`) instead of composing it inline.",
|
|
28
27
|
},
|
|
29
|
-
schema: [
|
|
30
|
-
{
|
|
31
|
-
type: "object",
|
|
32
|
-
properties: { testFiles: filePatternsSchema },
|
|
33
|
-
additionalProperties: false,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
28
|
+
schema: [],
|
|
36
29
|
},
|
|
37
30
|
createOnce(context) {
|
|
38
31
|
let inTestFile = false;
|
|
@@ -12,13 +12,24 @@ const defaultSecretFields = [
|
|
|
12
12
|
"refreshToken",
|
|
13
13
|
"idToken",
|
|
14
14
|
];
|
|
15
|
-
|
|
16
|
-
const
|
|
15
|
+
// `optionalWith` and `annotations` are the Effect 3 names.
|
|
16
|
+
const nullableWrappers = new Set([
|
|
17
|
+
"optional",
|
|
18
|
+
"optionalKey",
|
|
19
|
+
"optionalWith",
|
|
20
|
+
"NullOr",
|
|
21
|
+
"UndefinedOr",
|
|
22
|
+
"NullishOr",
|
|
23
|
+
]);
|
|
24
|
+
const chainMethods = new Set(["pipe", "check", "annotate", "annotations"]);
|
|
17
25
|
const isRedactedSchema = (node) => node?.type === "CallExpression" &&
|
|
18
26
|
(memberOf(node.callee, "Schema")?.startsWith("Redacted") ?? false);
|
|
27
|
+
// Effect 3 spells it `Config.redacted`, which also wraps a config: `Config.redacted(Config.string(…))`.
|
|
19
28
|
const isRedactedConfig = (node) => {
|
|
20
29
|
const name = calleeOf(node, "Config");
|
|
21
|
-
|
|
30
|
+
if (name === "Redacted" || name === "redacted")
|
|
31
|
+
return true;
|
|
32
|
+
return (name === "schema" && node?.type === "CallExpression" && isRedactedSchema(node.arguments[0]));
|
|
22
33
|
};
|
|
23
34
|
const isRedactedReference = (node) => (memberOf(node, "Schema") ?? calleeOf(node, "Schema"))?.startsWith("Redacted") ?? false;
|
|
24
35
|
// `Schema.String`, `Schema.NonEmptyString.check(…)`, `Schema.optional(Schema.String)`: a schema
|
|
@@ -66,7 +77,7 @@ export const redactedSecretNamesRule = defineRule({
|
|
|
66
77
|
description: "Read configuration values and declare Schema fields whose name looks like a secret as `Redacted`.",
|
|
67
78
|
},
|
|
68
79
|
messages: {
|
|
69
|
-
notRedacted: "`{{name}}` looks like a secret: read it with `Config.Redacted` or `Config.
|
|
80
|
+
notRedacted: "`{{name}}` looks like a secret: read it with `Config.schema(Schema.Redacted(...), name)` or, v3: `Config.redacted(name)`, v4: `Config.Redacted(name)`.",
|
|
70
81
|
fieldNotRedacted: "`{{name}}` looks like a secret: declare it as `Schema.Redacted(...)` so it stays out of logs.",
|
|
71
82
|
},
|
|
72
83
|
schema: [
|
|
@@ -82,7 +93,9 @@ export const redactedSecretNamesRule = defineRule({
|
|
|
82
93
|
createOnce(context) {
|
|
83
94
|
let secretFields = defaultSecretFields;
|
|
84
95
|
const checkConfig = (node) => {
|
|
85
|
-
if (calleeOf(node, "Config") === undefined
|
|
96
|
+
if (calleeOf(node, "Config") === undefined)
|
|
97
|
+
return;
|
|
98
|
+
if (isRedactedConfig(node) || isRedactedConfig(node.parent))
|
|
86
99
|
return;
|
|
87
100
|
for (const argument of node.arguments) {
|
|
88
101
|
if (argument.type === "Literal" &&
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { runnerName } from "../shared/effect-runners.js";
|
|
3
3
|
import { isEntryFile } from "../shared/entry-files.js";
|
|
4
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
5
4
|
import { isTestFile } from "../shared/test-files.js";
|
|
6
5
|
// Reports references too, not only calls: `const run = Effect.runPromise` escapes nothing.
|
|
7
6
|
export const runOnlyInRunnerRule = defineRule({
|
|
@@ -13,13 +12,7 @@ export const runOnlyInRunnerRule = defineRule({
|
|
|
13
12
|
messages: {
|
|
14
13
|
runOutsideEntry: "`{{name}}` runs an Effect program: only the entry point may do that.",
|
|
15
14
|
},
|
|
16
|
-
schema: [
|
|
17
|
-
{
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: { entryFiles: filePatternsSchema, testFiles: filePatternsSchema },
|
|
20
|
-
additionalProperties: false,
|
|
21
|
-
},
|
|
22
|
-
],
|
|
15
|
+
schema: [],
|
|
23
16
|
},
|
|
24
17
|
createOnce(context) {
|
|
25
18
|
let skipped = false;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf } from "../shared/effect-ast.js";
|
|
3
|
-
// `class X extends Context.Service<X, Shape>()(key)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
// `class X extends Context.Service<X, Shape>()(key)` and Effect 3 `Effect.Service<X>()(key, …)`:
|
|
4
|
+
// the outer call carries the key. Effect 3 `Context.Tag(key)<X, Shape>()`: the inner one does.
|
|
5
|
+
const serviceKey = (superClass) => {
|
|
6
|
+
if (superClass?.type !== "CallExpression")
|
|
7
|
+
return undefined;
|
|
8
|
+
const factory = superClass.callee;
|
|
9
|
+
if (calleeOf(factory, "Context") === "Service" || calleeOf(factory, "Effect") === "Service") {
|
|
10
|
+
return superClass.arguments[0];
|
|
11
|
+
}
|
|
12
|
+
return factory.type === "CallExpression" && calleeOf(factory, "Context") === "Tag"
|
|
13
|
+
? factory.arguments[0]
|
|
14
|
+
: undefined;
|
|
15
|
+
};
|
|
7
16
|
const literalKey = (node) => {
|
|
8
17
|
if (node?.type === "Literal" && typeof node.value === "string")
|
|
9
18
|
return node.value;
|
|
@@ -30,7 +39,7 @@ export const serviceKeyMatchesNameRule = defineRule({
|
|
|
30
39
|
meta: {
|
|
31
40
|
type: "suggestion",
|
|
32
41
|
docs: {
|
|
33
|
-
description: "Key a
|
|
42
|
+
description: "Key a service class with its own name, alone or after a path prefix (v3: `Context.Tag`/`Effect.Service`, v4: `Context.Service`).",
|
|
34
43
|
},
|
|
35
44
|
hasSuggestions: true,
|
|
36
45
|
messages: {
|
|
@@ -40,7 +49,7 @@ export const serviceKeyMatchesNameRule = defineRule({
|
|
|
40
49
|
},
|
|
41
50
|
createOnce(context) {
|
|
42
51
|
const checkClass = (node) => {
|
|
43
|
-
const keyNode =
|
|
52
|
+
const keyNode = serviceKey(node.superClass);
|
|
44
53
|
const key = literalKey(keyNode);
|
|
45
54
|
const name = className(node);
|
|
46
55
|
if (keyNode === undefined || key === undefined || name === undefined)
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { calleeOf, memberOf } from "../shared/effect-ast.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
["Context", "Tag"],
|
|
6
|
-
["Context", "GenericTag"],
|
|
7
|
-
];
|
|
8
|
-
const implementationStatics = new Set(["layer", "Default"]);
|
|
3
|
+
// `Live` is the Effect 3 convention for the static layer.
|
|
4
|
+
const implementationStatics = new Set(["layer", "Default", "Live"]);
|
|
9
5
|
// `class X extends Context.Service<X, Shape>()(id, options?)`: the outer call carries the id.
|
|
10
6
|
const contextServiceIdCall = (superClass) => superClass?.type === "CallExpression" && calleeOf(superClass.callee, "Context") === "Service"
|
|
11
7
|
? superClass
|
|
12
8
|
: undefined;
|
|
9
|
+
// Effect 3 `class X extends Context.Tag(id)<X, Shape>() {}`: shape only, like `Context.Service`.
|
|
10
|
+
const isContextTagClass = (superClass) => superClass?.type === "CallExpression" && calleeOf(superClass.callee, "Context") === "Tag";
|
|
13
11
|
const staticMemberName = (member) => (member.type === "PropertyDefinition" || member.type === "MethodDefinition") &&
|
|
14
12
|
member.static &&
|
|
15
13
|
member.key.type === "Identifier"
|
|
@@ -19,20 +17,20 @@ export const servicePortShapeOnlyRule = defineRule({
|
|
|
19
17
|
meta: {
|
|
20
18
|
type: "problem",
|
|
21
19
|
docs: {
|
|
22
|
-
description: "Keep a
|
|
20
|
+
description: "Keep a service port a pure contract (v3: `Context.Tag`, v4: `Context.Service`), with implementations in separate Layers.",
|
|
23
21
|
},
|
|
24
22
|
messages: {
|
|
25
23
|
makeOption: "Declare the port as shape only: move the implementation into a Layer in its own adapter file.",
|
|
26
24
|
staticImplementation: "Do not attach `static {{name}}` to the port: provide the implementation as a Layer in its own adapter file.",
|
|
27
|
-
|
|
25
|
+
effectService: "`Effect.Service` ties the port to its implementation: declare the port shape only (v3: `Context.Tag`, v4: `Context.Service`) and provide the implementation as a Layer in its own adapter file.",
|
|
28
26
|
},
|
|
29
27
|
},
|
|
30
28
|
createOnce(context) {
|
|
31
29
|
const checkClass = (node) => {
|
|
32
30
|
const idCall = contextServiceIdCall(node.superClass);
|
|
33
|
-
if (idCall === undefined)
|
|
31
|
+
if (idCall === undefined && !isContextTagClass(node.superClass))
|
|
34
32
|
return;
|
|
35
|
-
const options = idCall
|
|
33
|
+
const options = idCall?.arguments[1];
|
|
36
34
|
if (options !== undefined)
|
|
37
35
|
context.report({ node: options, messageId: "makeOption" });
|
|
38
36
|
for (const member of node.body.body) {
|
|
@@ -50,14 +48,8 @@ export const servicePortShapeOnlyRule = defineRule({
|
|
|
50
48
|
ClassDeclaration: checkClass,
|
|
51
49
|
ClassExpression: checkClass,
|
|
52
50
|
MemberExpression(node) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
context.report({
|
|
56
|
-
node,
|
|
57
|
-
messageId: "legacyService",
|
|
58
|
-
data: { name: `${object}.${property}` },
|
|
59
|
-
});
|
|
60
|
-
}
|
|
51
|
+
if (memberOf(node, "Effect") === "Service") {
|
|
52
|
+
context.report({ node, messageId: "effectService" });
|
|
61
53
|
}
|
|
62
54
|
},
|
|
63
55
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { isEffectGenerator, isServerComponent, memberOf, } from "../shared/effect-ast.js";
|
|
3
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
4
3
|
import { isTestFile } from "../shared/test-files.js";
|
|
5
4
|
const isThenCall = (node) => node.callee.type === "MemberExpression" &&
|
|
6
5
|
!node.callee.computed &&
|
|
@@ -49,13 +48,7 @@ export const singleStylePerFunctionRule = defineRule({
|
|
|
49
48
|
asyncEffectHook: "This hook chains promises around Effect: await a single program, lifting the promises into it with `Effect.tryPromise({ try, catch })`, or build the fixture as a Layer with `layer` from `@effect/vitest`.",
|
|
50
49
|
asyncEffectHelper: "This test helper chains promises around Effect: await a single program, lifting the promises into it with `Effect.tryPromise({ try, catch })`.",
|
|
51
50
|
},
|
|
52
|
-
schema: [
|
|
53
|
-
{
|
|
54
|
-
type: "object",
|
|
55
|
-
properties: { testFiles: filePatternsSchema },
|
|
56
|
-
additionalProperties: false,
|
|
57
|
-
},
|
|
58
|
-
],
|
|
51
|
+
schema: [],
|
|
59
52
|
},
|
|
60
53
|
createOnce(context) {
|
|
61
54
|
const frames = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const splitSuccessPathFromErrorHandlingRule: import("@oxlint/plugins").Rule;
|