@doubleloop-io/oxlint-plugin-effect 0.0.0-stage → 0.1.0-rc.1

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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.anti-slop +21 -0
  3. package/README.md +216 -2
  4. package/RULES.md +931 -0
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.js +75 -0
  7. package/dist/presets/all.d.ts +7 -0
  8. package/dist/presets/all.js +9 -0
  9. package/dist/presets/all.json +38 -0
  10. package/dist/rules/adapter-import-boundary.d.ts +7 -0
  11. package/dist/rules/adapter-import-boundary.js +103 -0
  12. package/dist/rules/all-errors-handled-at-boundary.d.ts +1 -0
  13. package/dist/rules/all-errors-handled-at-boundary.js +111 -0
  14. package/dist/rules/anti-slop-no-manual-effect-error-tag.d.ts +1 -0
  15. package/dist/rules/anti-slop-no-manual-effect-error-tag.js +32 -0
  16. package/dist/rules/anti-slop-no-service-constructor-imports.d.ts +1 -0
  17. package/dist/rules/anti-slop-no-service-constructor-imports.js +49 -0
  18. package/dist/rules/anti-slop-prefer-effect-match.d.ts +1 -0
  19. package/dist/rules/anti-slop-prefer-effect-match.js +45 -0
  20. package/dist/rules/error-handling-in-effect-fn-handler.d.ts +1 -0
  21. package/dist/rules/error-handling-in-effect-fn-handler.js +144 -0
  22. package/dist/rules/exported-schema-identifier.d.ts +1 -0
  23. package/dist/rules/exported-schema-identifier.js +117 -0
  24. package/dist/rules/no-asymmetric-match-handlers.d.ts +1 -0
  25. package/dist/rules/no-asymmetric-match-handlers.js +74 -0
  26. package/dist/rules/no-bare-error-in-effect.d.ts +1 -0
  27. package/dist/rules/no-bare-error-in-effect.js +49 -0
  28. package/dist/rules/no-effect-promise.d.ts +1 -0
  29. package/dist/rules/no-effect-promise.js +57 -0
  30. package/dist/rules/no-effect-return-type.d.ts +1 -0
  31. package/dist/rules/no-effect-return-type.js +130 -0
  32. package/dist/rules/no-inline-schema-codec.d.ts +1 -0
  33. package/dist/rules/no-inline-schema-codec.js +46 -0
  34. package/dist/rules/no-inline-yield-star.d.ts +1 -0
  35. package/dist/rules/no-inline-yield-star.js +230 -0
  36. package/dist/rules/no-manual-discriminant-comparison.d.ts +2 -0
  37. package/dist/rules/no-manual-discriminant-comparison.js +81 -0
  38. package/dist/rules/no-manual-discriminant-construction.d.ts +2 -0
  39. package/dist/rules/no-manual-discriminant-construction.js +132 -0
  40. package/dist/rules/no-nested-match.d.ts +1 -0
  41. package/dist/rules/no-nested-match.js +24 -0
  42. package/dist/rules/no-parameterless-catch-all.d.ts +1 -0
  43. package/dist/rules/no-parameterless-catch-all.js +29 -0
  44. package/dist/rules/no-provide-on-definition.d.ts +1 -0
  45. package/dist/rules/no-provide-on-definition.js +123 -0
  46. package/dist/rules/no-transport-types-in-generator.d.ts +1 -0
  47. package/dist/rules/no-transport-types-in-generator.js +63 -0
  48. package/dist/rules/no-yielded-match.d.ts +1 -0
  49. package/dist/rules/no-yielded-match.js +91 -0
  50. package/dist/rules/prefer-effect-fn.d.ts +1 -0
  51. package/dist/rules/prefer-effect-fn.js +102 -0
  52. package/dist/rules/prefer-effect-vitest.d.ts +1 -0
  53. package/dist/rules/prefer-effect-vitest.js +52 -0
  54. package/dist/rules/provide-named-layer.d.ts +1 -0
  55. package/dist/rules/provide-named-layer.js +55 -0
  56. package/dist/rules/redacted-secret-names.d.ts +1 -0
  57. package/dist/rules/redacted-secret-names.js +125 -0
  58. package/dist/rules/run-only-in-runner.d.ts +1 -0
  59. package/dist/rules/run-only-in-runner.js +40 -0
  60. package/dist/rules/service-built-with-of.d.ts +1 -0
  61. package/dist/rules/service-built-with-of.js +74 -0
  62. package/dist/rules/service-key-matches-name.d.ts +1 -0
  63. package/dist/rules/service-key-matches-name.js +69 -0
  64. package/dist/rules/service-port-shape-only.d.ts +1 -0
  65. package/dist/rules/service-port-shape-only.js +65 -0
  66. package/dist/rules/single-style-per-function.d.ts +1 -0
  67. package/dist/rules/single-style-per-function.js +145 -0
  68. package/dist/rules/try-object-form.d.ts +1 -0
  69. package/dist/rules/try-object-form.js +26 -0
  70. package/dist/rules/untraced-recursion.d.ts +1 -0
  71. package/dist/rules/untraced-recursion.js +48 -0
  72. package/dist/shared/effect-ast.d.ts +10 -0
  73. package/dist/shared/effect-ast.js +63 -0
  74. package/dist/shared/effect-runners.d.ts +2 -0
  75. package/dist/shared/effect-runners.js +14 -0
  76. package/dist/shared/entry-files.d.ts +6 -0
  77. package/dist/shared/entry-files.js +5 -0
  78. package/dist/shared/file-globs.d.ts +14 -0
  79. package/dist/shared/file-globs.js +17 -0
  80. package/dist/shared/match-ast.d.ts +5 -0
  81. package/dist/shared/match-ast.js +52 -0
  82. package/dist/shared/tagged-values.d.ts +17 -0
  83. package/dist/shared/tagged-values.js +67 -0
  84. package/dist/shared/test-files.d.ts +6 -0
  85. package/dist/shared/test-files.js +10 -0
  86. package/package.json +60 -3
@@ -0,0 +1,117 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { calleeOf } from "../shared/effect-ast.js";
3
+ // `Schema.Class` and `Schema.TaggedError` carry their name already.
4
+ const anonymousSchemas = new Set(["Struct", "Union", "Array"]);
5
+ const chainedMethod = (node) => node.type === "CallExpression" &&
6
+ node.callee.type === "MemberExpression" &&
7
+ !node.callee.computed &&
8
+ node.callee.property.type === "Identifier"
9
+ ? { name: node.callee.property.name, receiver: node.callee.object, call: node }
10
+ : undefined;
11
+ // `X.pipe(Schema.annotate(a))` and `X.annotate(a)`, in any order along the chain.
12
+ const annotationsOf = (init) => {
13
+ const annotations = [];
14
+ let current = init;
15
+ while (calleeOf(current, "Schema") === undefined) {
16
+ const link = chainedMethod(current);
17
+ if (link === undefined)
18
+ return undefined;
19
+ if (link.name === "annotate")
20
+ annotations.push(...link.call.arguments);
21
+ else if (link.name === "pipe") {
22
+ for (const argument of link.call.arguments) {
23
+ if (calleeOf(argument, "Schema") === "annotate" &&
24
+ argument.type === "CallExpression") {
25
+ annotations.push(...argument.arguments);
26
+ }
27
+ }
28
+ }
29
+ else if (link.name !== "check")
30
+ return undefined;
31
+ current = link.receiver;
32
+ }
33
+ const root = calleeOf(current, "Schema");
34
+ return root !== undefined && anonymousSchemas.has(root) ? annotations : undefined;
35
+ };
36
+ const identifierOf = (annotations) => {
37
+ for (const annotation of annotations) {
38
+ if (annotation.type !== "ObjectExpression")
39
+ continue;
40
+ for (const property of annotation.properties) {
41
+ if (property.type === "Property" &&
42
+ !property.computed &&
43
+ ((property.key.type === "Identifier" && property.key.name === "identifier") ||
44
+ (property.key.type === "Literal" && property.key.value === "identifier"))) {
45
+ return property.value;
46
+ }
47
+ }
48
+ }
49
+ return undefined;
50
+ };
51
+ export const exportedSchemaIdentifierRule = defineRule({
52
+ meta: {
53
+ type: "suggestion",
54
+ docs: {
55
+ description: "Annotate every exported `Schema.Struct`, `Schema.Union` and `Schema.Array` with an `identifier` equal to its name.",
56
+ },
57
+ hasSuggestions: true,
58
+ messages: {
59
+ missingIdentifier: 'Annotate `{{name}}` with `identifier: "{{name}}"`, so errors, JSON Schema and arbitraries name it.',
60
+ wrongIdentifier: 'The `identifier` of `{{name}}` is `{{identifier}}`: use `"{{name}}"`, the name the schema is imported by.',
61
+ addIdentifier: 'Append `.annotate({ identifier: "{{name}}" })`.',
62
+ renameIdentifier: 'Use `"{{name}}"` as identifier.',
63
+ },
64
+ },
65
+ createOnce(context) {
66
+ const check = (declarator) => {
67
+ const { id, init } = declarator;
68
+ if (id.type !== "Identifier" || !init)
69
+ return;
70
+ const annotations = annotationsOf(init);
71
+ if (annotations === undefined)
72
+ return;
73
+ const name = id.name;
74
+ const identifier = identifierOf(annotations);
75
+ if (identifier === undefined) {
76
+ context.report({
77
+ node: id,
78
+ messageId: "missingIdentifier",
79
+ data: { name },
80
+ suggest: [
81
+ {
82
+ messageId: "addIdentifier",
83
+ data: { name },
84
+ fix: (fixer) => fixer.insertTextAfter(init, `.annotate({ identifier: "${name}" })`),
85
+ },
86
+ ],
87
+ });
88
+ return;
89
+ }
90
+ // A computed identifier cannot be checked without evaluating it.
91
+ if (identifier.type !== "Literal" || typeof identifier.value !== "string")
92
+ return;
93
+ if (identifier.value === name)
94
+ return;
95
+ context.report({
96
+ node: identifier,
97
+ messageId: "wrongIdentifier",
98
+ data: { name, identifier: identifier.value },
99
+ suggest: [
100
+ {
101
+ messageId: "renameIdentifier",
102
+ data: { name },
103
+ fix: (fixer) => fixer.replaceText(identifier, `"${name}"`),
104
+ },
105
+ ],
106
+ });
107
+ };
108
+ return {
109
+ // Internal fragments, reused through `.fields`, never reach an error message alone.
110
+ ExportNamedDeclaration(node) {
111
+ if (node.declaration?.type !== "VariableDeclaration")
112
+ return;
113
+ node.declaration.declarations.forEach(check);
114
+ },
115
+ };
116
+ },
117
+ });
@@ -0,0 +1 @@
1
+ export declare const noAsymmetricMatchHandlersRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,74 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { calleeOf, isEffectFnApplication, isFunctionNode } from "../shared/effect-ast.js";
3
+ import { handlersOf, isMatchCall } from "../shared/match-ast.js";
4
+ const isDelegation = (node) => node?.type === "Identifier" ||
5
+ node?.type === "MemberExpression" ||
6
+ node?.type === "CallExpression" ||
7
+ node?.type === "NewExpression";
8
+ const isEffectBody = (node) => node.type === "CallExpression" &&
9
+ (isEffectFnApplication(node) || calleeOf(node, "Effect") === "gen") &&
10
+ node.arguments.some(isFunctionNode);
11
+ const shapeOfResult = (node) => {
12
+ if (!node)
13
+ return "value";
14
+ if (isEffectBody(node))
15
+ return "inline";
16
+ return isDelegation(node) ? "delegating" : "value";
17
+ };
18
+ // Literals, templates, arrays and ternaries produce a value rather than implement a step,
19
+ // so they sit on either side of an asymmetry without breaking it.
20
+ const shapeOf = (handler) => {
21
+ if (!isFunctionNode(handler))
22
+ return shapeOfResult(handler);
23
+ if (!handler.body)
24
+ return "value";
25
+ if (handler.generator)
26
+ return "inline";
27
+ if (handler.body.type !== "BlockStatement")
28
+ return shapeOfResult(handler.body);
29
+ const [only, ...rest] = handler.body.body;
30
+ if (!only)
31
+ return "value";
32
+ if (rest.length > 0 || only.type !== "ReturnStatement")
33
+ return "inline";
34
+ return shapeOfResult(only.argument);
35
+ };
36
+ const effectFnName = (handler) => {
37
+ if (handler.type !== "CallExpression" || handler.callee.type !== "CallExpression")
38
+ return;
39
+ const [name] = handler.callee.arguments;
40
+ return name?.type === "Literal" && typeof name.value === "string" ? name.value : undefined;
41
+ };
42
+ export const noAsymmetricMatchHandlersRule = defineRule({
43
+ meta: {
44
+ type: "suggestion",
45
+ docs: {
46
+ description: "Keep the handlers of a match at one level of abstraction: when one delegates to a named function, extract the inline ones too.",
47
+ },
48
+ messages: {
49
+ inlineHandler: "This handler is implemented inline while another branch of the same match delegates to a named function. Extract it into a named function{{hint}} and call it from the handler.",
50
+ },
51
+ },
52
+ createOnce(context) {
53
+ return {
54
+ CallExpression(node) {
55
+ if (!isMatchCall(node))
56
+ return;
57
+ const handlers = handlersOf(node);
58
+ const shapes = handlers.map(shapeOf);
59
+ if (!shapes.includes("delegating"))
60
+ return;
61
+ handlers.forEach((handler, index) => {
62
+ if (shapes[index] !== "inline")
63
+ return;
64
+ const name = effectFnName(handler);
65
+ context.report({
66
+ node: handler,
67
+ messageId: "inlineHandler",
68
+ data: { hint: name ? ` (\`${name}\`)` : "" },
69
+ });
70
+ });
71
+ },
72
+ };
73
+ },
74
+ });
@@ -0,0 +1 @@
1
+ export declare const noBareErrorInEffectRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,49 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { calleeOf, isEffectGenerator, nearestFunction } from "../shared/effect-ast.js";
3
+ import { filePatternsSchema } from "../shared/file-globs.js";
4
+ import { isTestFile } from "../shared/test-files.js";
5
+ // Only the two forms the effecttsgo preset misses: `Effect.fail(new Error(…))` and
6
+ // `extends Error` are already reported by global-error-in-effect-failure and extends-native-error.
7
+ export const noBareErrorInEffectRule = defineRule({
8
+ meta: {
9
+ type: "problem",
10
+ docs: {
11
+ description: "Fail Effects with tagged errors, never with a `throw` or a primitive value. Primitive failures in test files are allowed.",
12
+ },
13
+ messages: {
14
+ throwInGenerator: "A `throw` inside an Effect generator becomes a defect: `return yield* new TaggedError(...)` instead.",
15
+ primitiveFailure: "Fail with a tagged error (`Data.TaggedError` or `Schema.TaggedError`), not a primitive value.",
16
+ },
17
+ schema: [
18
+ {
19
+ type: "object",
20
+ properties: { testFiles: filePatternsSchema },
21
+ additionalProperties: false,
22
+ },
23
+ ],
24
+ },
25
+ createOnce(context) {
26
+ let inTestFile = false;
27
+ return {
28
+ // A test's synthetic failure (`Effect.fail("boom")`) feeds code generic over the error,
29
+ // which never discriminates it; a `throw` is still a defect there.
30
+ before() {
31
+ inTestFile = isTestFile(context);
32
+ },
33
+ ThrowStatement(node) {
34
+ const fn = nearestFunction(node);
35
+ if (fn !== undefined && isEffectGenerator(fn)) {
36
+ context.report({ node, messageId: "throwInGenerator" });
37
+ }
38
+ },
39
+ CallExpression(node) {
40
+ if (inTestFile || calleeOf(node, "Effect") !== "fail")
41
+ return;
42
+ const [failure] = node.arguments;
43
+ if (failure?.type === "Literal" || failure?.type === "TemplateLiteral") {
44
+ context.report({ node: failure, messageId: "primitiveFailure" });
45
+ }
46
+ },
47
+ };
48
+ },
49
+ });
@@ -0,0 +1 @@
1
+ export declare const noEffectPromiseRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,57 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { calleeOf } from "../shared/effect-ast.js";
3
+ import { filePatternsSchema } from "../shared/file-globs.js";
4
+ import { isTestFile } from "../shared/test-files.js";
5
+ // Argument index of the release step, where a promise that cannot reject is the normal case.
6
+ const releaseSlots = new Map([
7
+ ["acquireRelease", 1],
8
+ ["acquireUseRelease", 2],
9
+ ["addFinalizer", 0],
10
+ ]);
11
+ const isInsideRelease = (node) => {
12
+ let child = node;
13
+ for (let current = node.parent; current; current = current.parent) {
14
+ const slot = releaseSlots.get(calleeOf(current, "Effect") ?? "");
15
+ if (current.type === "CallExpression" &&
16
+ slot !== undefined &&
17
+ current.arguments[slot] === child) {
18
+ return true;
19
+ }
20
+ child = current;
21
+ }
22
+ return false;
23
+ };
24
+ export const noEffectPromiseRule = defineRule({
25
+ meta: {
26
+ type: "suggestion",
27
+ docs: {
28
+ description: "Prefer `Effect.tryPromise` over `Effect.promise` outside resource release. Test files are skipped.",
29
+ },
30
+ messages: {
31
+ rejectionAsDefect: "`Effect.promise` turns a rejection into a defect: use `Effect.tryPromise({ try, catch })` unless the promise cannot reject.",
32
+ },
33
+ schema: [
34
+ {
35
+ type: "object",
36
+ properties: { testFiles: filePatternsSchema },
37
+ additionalProperties: false,
38
+ },
39
+ ],
40
+ },
41
+ createOnce(context) {
42
+ let inTestFile = false;
43
+ return {
44
+ // A rejection during test setup is a broken test, and a defect fails it with the cause.
45
+ before() {
46
+ inTestFile = isTestFile(context);
47
+ },
48
+ CallExpression(node) {
49
+ if (inTestFile)
50
+ return;
51
+ if (calleeOf(node, "Effect") === "promise" && !isInsideRelease(node)) {
52
+ context.report({ node, messageId: "rejectionAsDefect" });
53
+ }
54
+ },
55
+ };
56
+ },
57
+ });
@@ -0,0 +1 @@
1
+ export declare const noEffectReturnTypeRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,130 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { nearestFunction } from "../shared/effect-ast.js";
3
+ // `Effect.Effect`, `Stream.Stream`, `Layer.Layer`: the types that carry error and requirement
4
+ // channels collected from everything they call.
5
+ const channelModules = new Set(["Effect", "Stream", "Layer"]);
6
+ const isChannelType = (node) => node.typeName.type === "TSQualifiedName" &&
7
+ node.typeName.left.type === "Identifier" &&
8
+ channelModules.has(node.typeName.left.name) &&
9
+ node.typeName.right.name === node.typeName.left.name;
10
+ const isInside = (node, ancestor) => {
11
+ for (let current = node.parent; current; current = current.parent) {
12
+ if (current === ancestor)
13
+ return true;
14
+ }
15
+ return false;
16
+ };
17
+ const branchesOf = (expression) => expression.type === "ConditionalExpression"
18
+ ? [...branchesOf(expression.consequent), ...branchesOf(expression.alternate)]
19
+ : [expression];
20
+ // Without type information, branches built differently (`Effect.succeed` and `Effect.fail`)
21
+ // stand in for an inferred union of effects that the annotation folds into one type.
22
+ const returnsDifferentShapes = (branches, sourceCode) => {
23
+ const shapeOf = (branch) => branch.type === "CallExpression" || branch.type === "NewExpression"
24
+ ? `${branch.type} ${sourceCode.getText(branch.callee)}`
25
+ : branch.type;
26
+ return new Set(branches.map(shapeOf)).size > 1;
27
+ };
28
+ // TypeScript cannot infer the return type of a function that calls itself: it falls back to
29
+ // `any` and asks for the annotation.
30
+ const isRecursive = (node, sourceCode) => {
31
+ const declaration = node.type === "FunctionDeclaration" ? node : node.parent;
32
+ const name = declaration?.type === "FunctionDeclaration"
33
+ ? declaration.id?.name
34
+ : declaration?.type === "VariableDeclarator" && declaration.id.type === "Identifier"
35
+ ? declaration.id.name
36
+ : undefined;
37
+ if (declaration === null || name === undefined)
38
+ return false;
39
+ // The declared variables of a function declaration include its parameters.
40
+ return sourceCode
41
+ .getDeclaredVariables(declaration)
42
+ .some((variable) => variable.name === name &&
43
+ variable.references.some((reference) => isInside(reference.identifier, node)));
44
+ };
45
+ // Only the declaration itself: methods of an exported object and functions it returns are not
46
+ // part of the module's signature.
47
+ const isExportedDeclaration = (node) => {
48
+ const declarator = node.parent;
49
+ const declaration = declarator?.type === "VariableDeclarator" && declarator.init === node
50
+ ? declarator.parent
51
+ : node;
52
+ const parent = declaration?.parent?.type;
53
+ return parent === "ExportNamedDeclaration" || parent === "ExportDefaultDeclaration";
54
+ };
55
+ const checksExported = (options) => {
56
+ const [first] = options;
57
+ return typeof first === "object" && first !== null && "checkExported" in first
58
+ ? first.checkExported === true
59
+ : false;
60
+ };
61
+ export const noEffectReturnTypeRule = defineRule({
62
+ meta: {
63
+ type: "suggestion",
64
+ docs: {
65
+ description: "Let inference type a function returning an Effect: its error and requirement channels change with every callee. Exported declarations are skipped: their annotation is a contract.",
66
+ },
67
+ hasSuggestions: true,
68
+ messages: {
69
+ effectReturnType: "Remove the return type annotation: error and requirement channels follow the callees, so inference keeps them up to date.",
70
+ removeReturnType: "Remove the return type annotation.",
71
+ },
72
+ schema: [
73
+ {
74
+ type: "object",
75
+ properties: { checkExported: { type: "boolean" } },
76
+ additionalProperties: false,
77
+ },
78
+ ],
79
+ },
80
+ createOnce(context) {
81
+ let checkExported = false;
82
+ const annotated = new Set();
83
+ const returned = new Map();
84
+ const collect = (node) => {
85
+ const fn = nearestFunction(node);
86
+ if (fn === undefined || node.argument === null)
87
+ return;
88
+ returned.set(fn, [...(returned.get(fn) ?? []), node.argument]);
89
+ };
90
+ const check = (node) => {
91
+ const statements = returned.get(node) ?? [];
92
+ returned.delete(node);
93
+ if (!annotated.delete(node) || !node.returnType)
94
+ return;
95
+ const expressions = node.body?.type === "BlockStatement" ? statements : node.body ? [node.body] : [];
96
+ if (returnsDifferentShapes(expressions.flatMap(branchesOf), context.sourceCode))
97
+ return;
98
+ if (isRecursive(node, context.sourceCode))
99
+ return;
100
+ if (!checkExported && isExportedDeclaration(node))
101
+ return;
102
+ const { returnType } = node;
103
+ context.report({
104
+ node: returnType,
105
+ messageId: "effectReturnType",
106
+ suggest: [
107
+ {
108
+ messageId: "removeReturnType",
109
+ fix: (fixer) => fixer.remove(returnType),
110
+ },
111
+ ],
112
+ });
113
+ };
114
+ return {
115
+ before() {
116
+ checkExported = checksExported(context.options);
117
+ },
118
+ TSTypeReference(node) {
119
+ const fn = nearestFunction(node);
120
+ if (fn?.returnType && isChannelType(node) && isInside(node, fn.returnType)) {
121
+ annotated.add(fn);
122
+ }
123
+ },
124
+ ReturnStatement: collect,
125
+ "ArrowFunctionExpression:exit": check,
126
+ "FunctionDeclaration:exit": check,
127
+ "FunctionExpression:exit": check,
128
+ };
129
+ },
130
+ });
@@ -0,0 +1 @@
1
+ export declare const noInlineSchemaCodecRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,46 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { calleeOf, nearestFunction } from "../shared/effect-ast.js";
3
+ // Only the codec factories: `Schema.decode`, `decodeTo`, `encodeKeys`, … build schemas.
4
+ const codecFactory = /^(decode|encode)(Unknown)?(Effect|Exit|Option|Promise|Result|Sync)$/;
5
+ const layerConstructors = new Set(["effect", "scoped"]);
6
+ const isLayerConstructor = (node) => {
7
+ const name = calleeOf(node, "Layer");
8
+ return name !== undefined && layerConstructors.has(name);
9
+ };
10
+ // `Layer.effect(T, Effect.gen(…))` and the curried `Layer.effect(T)(Effect.gen(…))`: the
11
+ // generator runs once, when the Layer is built.
12
+ const isLayerConstructionGenerator = (fn) => {
13
+ const gen = fn.parent;
14
+ if (!fn.generator || gen?.type !== "CallExpression" || calleeOf(gen, "Effect") !== "gen") {
15
+ return false;
16
+ }
17
+ const layer = gen.parent;
18
+ if (layer?.type !== "CallExpression" || !layer.arguments.some((arg) => arg === gen)) {
19
+ return false;
20
+ }
21
+ return isLayerConstructor(layer) || isLayerConstructor(layer.callee);
22
+ };
23
+ export const noInlineSchemaCodecRule = defineRule({
24
+ meta: {
25
+ type: "suggestion",
26
+ docs: {
27
+ description: "Extract `Schema.decode*`/`Schema.encode*` codecs into module-level constants instead of applying them inline.",
28
+ },
29
+ messages: {
30
+ inlineCodec: "`Schema.{{factory}}(…)` is built and applied inline: extract it into a named module-level constant.",
31
+ },
32
+ },
33
+ createOnce(context) {
34
+ return {
35
+ CallExpression(node) {
36
+ const factory = calleeOf(node.callee, "Schema");
37
+ if (factory === undefined || !codecFactory.test(factory))
38
+ return;
39
+ const fn = nearestFunction(node);
40
+ if (fn === undefined || isLayerConstructionGenerator(fn))
41
+ return;
42
+ context.report({ node: node.callee, messageId: "inlineCodec", data: { factory } });
43
+ },
44
+ };
45
+ },
46
+ });
@@ -0,0 +1 @@
1
+ export declare const noInlineYieldStarRule: import("@oxlint/plugins").Rule;