@doubleloop-io/oxlint-plugin-effect 0.1.0-rc.0 → 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 +106 -82
- package/RULES.md +527 -382
- package/dist/index.d.ts +3 -0
- package/dist/index.js +13 -8
- package/dist/presets/all.json +5 -4
- package/dist/rules/adapter-import-boundary.js +5 -12
- package/dist/rules/all-errors-handled-at-boundary.d.ts +1 -0
- package/dist/rules/all-errors-handled-at-boundary.js +105 -0
- 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 +36 -19
- package/dist/rules/no-manual-discriminant-construction.js +50 -14
- package/dist/rules/no-provide-on-definition.js +31 -9
- 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} +11 -3
- package/dist/rules/no-yielded-match.js +9 -2
- package/dist/rules/prefer-effect-vitest.d.ts +1 -0
- package/dist/rules/prefer-effect-vitest.js +45 -0
- package/dist/rules/provide-named-layer.js +1 -8
- package/dist/rules/redacted-secret-names.js +18 -7
- package/dist/rules/run-only-in-runner.d.ts +1 -0
- package/dist/rules/run-only-in-runner.js +33 -0
- 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 +82 -21
- package/dist/rules/split-success-path-from-error-handling.d.ts +1 -0
- package/dist/rules/split-success-path-from-error-handling.js +137 -0
- package/dist/shared/effect-ast.d.ts +1 -0
- package/dist/shared/effect-ast.js +15 -4
- package/dist/shared/effect-runners.d.ts +2 -0
- package/dist/shared/effect-runners.js +14 -0
- package/dist/shared/entry-files.d.ts +2 -2
- 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 +2 -2
- package/package.json +61 -54
- package/dist/rules/boundary-entrypoint-shape.d.ts +0 -1
- package/dist/rules/boundary-entrypoint-shape.js +0 -52
- package/dist/rules/error-handling-in-effect-fn-handler.d.ts +0 -1
- package/dist/rules/error-handling-in-effect-fn-handler.js +0 -66
- package/dist/rules/no-bare-error-in-effect.d.ts +0 -1
- package/dist/rules/run-only-at-entry.d.ts +0 -1
- package/dist/rules/run-only-at-entry.js +0 -50
|
@@ -28,8 +28,12 @@ export const isEffectGenerator = (fn) => {
|
|
|
28
28
|
return call.arguments.some((arg) => arg === fn);
|
|
29
29
|
return isEffectFnApplication(call) && call.arguments[0] === fn;
|
|
30
30
|
};
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const isEffectCombinatorCallback = (fn) => fn.parent?.type === "CallExpression" &&
|
|
32
|
+
memberOf(fn.parent.callee, "Effect") !== undefined &&
|
|
33
|
+
fn.parent.arguments.some((arg) => arg === fn);
|
|
34
|
+
// Callbacks handed to an `Effect.*` combinator inside the generator still belong to its success
|
|
35
|
+
// path; the pipeable arguments of an `Effect.fn` nested in it are the handler slot, so they do
|
|
36
|
+
// not, and neither does a function the generator only defines (an HTTP handler, a service method).
|
|
33
37
|
export const isInsideEffectGeneratorBody = (node) => {
|
|
34
38
|
let child = node;
|
|
35
39
|
for (let current = node.parent; current; current = current.parent) {
|
|
@@ -37,8 +41,12 @@ export const isInsideEffectGeneratorBody = (node) => {
|
|
|
37
41
|
current.arguments.findIndex((arg) => arg === child) > 0) {
|
|
38
42
|
return false;
|
|
39
43
|
}
|
|
40
|
-
if (isFunctionNode(current)
|
|
41
|
-
|
|
44
|
+
if (isFunctionNode(current)) {
|
|
45
|
+
if (current.generator)
|
|
46
|
+
return isEffectGenerator(current);
|
|
47
|
+
if (!isEffectCombinatorCallback(current))
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
42
50
|
child = current;
|
|
43
51
|
}
|
|
44
52
|
return false;
|
|
@@ -50,3 +58,6 @@ export const nearestFunction = (node) => {
|
|
|
50
58
|
}
|
|
51
59
|
return undefined;
|
|
52
60
|
};
|
|
61
|
+
// An async function that renders JSX is a React Server Component: it hands its outcome to React,
|
|
62
|
+
// and how failures become UI is the application's call, not a rule's.
|
|
63
|
+
export const isServerComponent = (fn, rendersJsx) => fn.async && rendersJsx;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { memberOf } from "./effect-ast.js";
|
|
2
|
+
const runtimeEntries = [
|
|
3
|
+
["NodeRuntime", "runMain"],
|
|
4
|
+
["BunRuntime", "runMain"],
|
|
5
|
+
["ManagedRuntime", "make"],
|
|
6
|
+
];
|
|
7
|
+
// The qualified name of the runner `node` refers to, when it runs an Effect program.
|
|
8
|
+
export const runnerName = (node) => {
|
|
9
|
+
const effectMember = memberOf(node, "Effect");
|
|
10
|
+
if (effectMember?.startsWith("run"))
|
|
11
|
+
return `Effect.${effectMember}`;
|
|
12
|
+
const entry = runtimeEntries.find(([object, property]) => memberOf(node, object) === property);
|
|
13
|
+
return entry === undefined ? undefined : `${entry[0]}.${entry[1]}`;
|
|
14
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export declare const defaultEntryFiles:
|
|
1
|
+
export declare const defaultEntryFiles: readonly ["**/main.ts", "**/cli.ts", "**/bin/**"];
|
|
2
2
|
export declare const isEntryFile: (context: {
|
|
3
3
|
readonly filename: string;
|
|
4
4
|
readonly cwd: string;
|
|
5
|
-
readonly
|
|
5
|
+
readonly settings: Readonly<Record<string, unknown>>;
|
|
6
6
|
}) => boolean;
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
type: string;
|
|
3
|
-
items: {
|
|
4
|
-
type: string;
|
|
5
|
-
};
|
|
6
|
-
uniqueItems: boolean;
|
|
7
|
-
};
|
|
1
|
+
import { type FileSettingKey } from "./settings.ts";
|
|
8
2
|
type FileContext = {
|
|
9
3
|
readonly filename: string;
|
|
10
4
|
readonly cwd: string;
|
|
11
|
-
readonly
|
|
5
|
+
readonly settings: Readonly<Record<string, unknown>>;
|
|
12
6
|
};
|
|
13
|
-
export declare const makeFileMatcher: (key:
|
|
7
|
+
export declare const makeFileMatcher: (key: FileSettingKey, defaults: ReadonlyArray<string>) => (context: FileContext) => boolean;
|
|
14
8
|
export {};
|
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { matchesGlob, relative, sep } from "node:path";
|
|
2
|
-
|
|
3
|
-
const configuredPatterns = (options, key, defaults) => {
|
|
4
|
-
const [first] = options;
|
|
5
|
-
if (typeof first !== "object" || first === null)
|
|
6
|
-
return defaults;
|
|
7
|
-
const entry = Object.entries(first).find(([name]) => name === key);
|
|
8
|
-
if (entry === undefined)
|
|
9
|
-
return defaults;
|
|
10
|
-
const patterns = entry[1];
|
|
11
|
-
return Array.isArray(patterns) ? patterns.filter((pattern) => typeof pattern === "string") : [];
|
|
12
|
-
};
|
|
2
|
+
import { projectSetting } from "./settings.js";
|
|
13
3
|
// Globs match the path relative to the working directory, with `/` on every platform.
|
|
14
4
|
export const makeFileMatcher = (key, defaults) => (context) => {
|
|
15
5
|
const path = relative(context.cwd, context.filename).split(sep).join("/");
|
|
16
|
-
|
|
6
|
+
const patterns = projectSetting(context.settings, key) ?? defaults;
|
|
7
|
+
return patterns.some((pattern) => matchesGlob(path, pattern));
|
|
17
8
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
declare const settingKeys: readonly ["entryFiles", "testFiles", "discriminants"];
|
|
2
|
+
type SettingKey = (typeof settingKeys)[number];
|
|
3
|
+
export type FileSettingKey = Exclude<SettingKey, "discriminants">;
|
|
4
|
+
export declare const projectSetting: (settings: Readonly<Record<string, unknown>>, key: SettingKey) => readonly string[] | undefined;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const settingKeys = ["entryFiles", "testFiles", "discriminants"];
|
|
2
|
+
const isSettingKey = (name) => settingKeys.some((key) => key === name);
|
|
3
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
4
|
+
const invalid = (problem) => new Error(`Invalid settings["dl-effect"]: ${problem}. Known keys: ${settingKeys.join(", ")}.`);
|
|
5
|
+
// Every key is checked on every read, not only the requested one: a typo surfaces as soon as any
|
|
6
|
+
// enabled rule reads a setting, even when no rule reads the misspelled key.
|
|
7
|
+
export const projectSetting = (settings, key) => {
|
|
8
|
+
const pluginSettings = settings["dl-effect"];
|
|
9
|
+
if (pluginSettings === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (typeof pluginSettings !== "object" ||
|
|
12
|
+
pluginSettings === null ||
|
|
13
|
+
Array.isArray(pluginSettings)) {
|
|
14
|
+
throw invalid("expected an object");
|
|
15
|
+
}
|
|
16
|
+
let configured;
|
|
17
|
+
for (const [name, value] of Object.entries(pluginSettings)) {
|
|
18
|
+
if (!isSettingKey(name))
|
|
19
|
+
throw invalid(`unknown key "${name}"`);
|
|
20
|
+
if (!isStringArray(value))
|
|
21
|
+
throw invalid(`"${name}" must be an array of strings`);
|
|
22
|
+
if (name === key)
|
|
23
|
+
configured = value;
|
|
24
|
+
}
|
|
25
|
+
return configured;
|
|
26
|
+
};
|
|
@@ -6,12 +6,6 @@ export declare const isReasonTagMember: (node: ESTree.MemberExpression) => boole
|
|
|
6
6
|
export declare const tagMemberFromComparison: (node: ESTree.BinaryExpression) => ESTree.MemberExpression | undefined;
|
|
7
7
|
export declare const isInsideBroadEffectHandler: (node: ESTree.Node) => boolean;
|
|
8
8
|
export declare const propertyName: (property: ESTree.ObjectProperty) => string | undefined;
|
|
9
|
-
export declare const
|
|
10
|
-
|
|
11
|
-
readonly items: {
|
|
12
|
-
readonly type: "string";
|
|
13
|
-
};
|
|
14
|
-
readonly uniqueItems: true;
|
|
15
|
-
};
|
|
16
|
-
export declare const configuredDiscriminants: (options: ReadonlyArray<unknown>) => Set<string>;
|
|
9
|
+
export declare const defaultDiscriminants: readonly ["_tag"];
|
|
10
|
+
export declare const configuredDiscriminants: (settings: Readonly<Record<string, unknown>>) => Set<string>;
|
|
17
11
|
export declare const discriminantOf: (node: ESTree.Node | null | undefined, discriminants: ReadonlySet<string>) => string | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { calleeOf } from "./effect-ast.js";
|
|
2
|
+
import { projectSetting } from "./settings.js";
|
|
2
3
|
const equalityOperators = new Set(["==", "===", "!=", "!=="]);
|
|
3
4
|
const broadEffectCatchMethods = new Set(["catch", "catchAll", "catchIf"]);
|
|
4
5
|
export const isEqualityComparison = (node) => node.type === "BinaryExpression" && equalityOperators.has(node.operator);
|
|
@@ -39,20 +40,8 @@ export const propertyName = (property) => {
|
|
|
39
40
|
};
|
|
40
41
|
// The fields that discriminate a closed union: `_tag` for Effect's tagged values, plus whatever
|
|
41
42
|
// the codebase uses (`type`, `kind`). Shared by the rules on comparison and construction.
|
|
42
|
-
const defaultDiscriminants = ["_tag"];
|
|
43
|
-
export const
|
|
44
|
-
type: "array",
|
|
45
|
-
items: { type: "string" },
|
|
46
|
-
uniqueItems: true,
|
|
47
|
-
};
|
|
48
|
-
export const configuredDiscriminants = (options) => {
|
|
49
|
-
const [first] = options;
|
|
50
|
-
if (typeof first !== "object" || first === null || !("discriminants" in first)) {
|
|
51
|
-
return new Set(defaultDiscriminants);
|
|
52
|
-
}
|
|
53
|
-
const names = first.discriminants;
|
|
54
|
-
return new Set(Array.isArray(names) ? names.filter((name) => typeof name === "string") : []);
|
|
55
|
-
};
|
|
43
|
+
export const defaultDiscriminants = ["_tag"];
|
|
44
|
+
export const configuredDiscriminants = (settings) => new Set(projectSetting(settings, "discriminants") ?? defaultDiscriminants);
|
|
56
45
|
export const discriminantOf = (node, discriminants) => {
|
|
57
46
|
if (node?.type !== "MemberExpression")
|
|
58
47
|
return undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export declare const defaultTestFiles:
|
|
1
|
+
export declare const defaultTestFiles: readonly ["**/*.test.*", "**/*.spec.*", "**/__tests__/**", "**/test/**", "**/tests/**"];
|
|
2
2
|
export declare const isTestFile: (context: {
|
|
3
3
|
readonly filename: string;
|
|
4
4
|
readonly cwd: string;
|
|
5
|
-
readonly
|
|
5
|
+
readonly settings: Readonly<Record<string, unknown>>;
|
|
6
6
|
}) => boolean;
|
package/package.json
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"name": "@doubleloop-io/oxlint-plugin-effect",
|
|
3
|
+
"version": "0.1.0-rc.2",
|
|
4
|
+
"description": "Oxlint rules for Effect 4 code",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/doubleloop-io/oxlint-plugin-effect.git"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"RULES.md",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"LICENSE.anti-slop"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
9
22
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"README.md",
|
|
14
|
-
"LICENSE",
|
|
15
|
-
"LICENSE.anti-slop"
|
|
16
|
-
],
|
|
17
|
-
"type": "module",
|
|
18
|
-
"exports": {
|
|
19
|
-
"./presets/all": {
|
|
20
|
-
"types": "./dist/presets/all.d.ts",
|
|
21
|
-
"default": "./dist/presets/all.js"
|
|
22
|
-
},
|
|
23
|
-
"./presets/all.json": "./dist/presets/all.json"
|
|
23
|
+
"./presets/all": {
|
|
24
|
+
"types": "./dist/presets/all.d.ts",
|
|
25
|
+
"default": "./dist/presets/all.js"
|
|
24
26
|
},
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
27
|
+
"./presets/all.json": "./dist/presets/all.json"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@oxlint/plugins": "^1.82.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.13.6",
|
|
37
|
+
"oxfmt": "0.67.0",
|
|
38
|
+
"oxlint": "1.82.0",
|
|
39
|
+
"publint": "^0.3.25",
|
|
40
|
+
"typescript": "^7.0.2"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"oxlint": "^1.82.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": "^22.20.0 || >=24.8.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"format": "oxfmt",
|
|
50
|
+
"format:check": "oxfmt --check",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"lint": "oxlint --deny-warnings",
|
|
53
|
+
"test": "node --test rules-md.test.ts rules/*.test.ts shared/*.test.ts scripts/*.test.ts",
|
|
54
|
+
"check": "pnpm format:check && pnpm typecheck && pnpm lint && pnpm test",
|
|
55
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && node presets/all-json.ts",
|
|
56
|
+
"build:w": "node --watch-path=index.ts --watch-path=rules --watch-path=presets --watch-path=shared --watch-path=tsconfig.build.json scripts/build.ts",
|
|
57
|
+
"pack:check": "node --test presets/all-dist.test.ts && node scripts/pack-check.ts",
|
|
58
|
+
"build-and-pack": "pnpm check && pnpm build && pnpm pack:check && pnpm smoke-test",
|
|
59
|
+
"smoke-test": "node scripts/smoke-test.ts",
|
|
60
|
+
"release": "node scripts/release.ts",
|
|
61
|
+
"consumer:link": "./scripts/link-consumer.sh link",
|
|
62
|
+
"consumer:unlink": "./scripts/link-consumer.sh unlink"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const boundaryEntrypointShapeRule: import("@oxlint/plugins").Rule;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import { calleeOf, nearestFunction } from "../shared/effect-ast.js";
|
|
3
|
-
// The *Exit runners hand failures back as values, so an open error channel cannot leak there.
|
|
4
|
-
const effectRunners = new Set(["runPromise", "runSync", "runFork", "runCallback"]);
|
|
5
|
-
const runtimes = ["NodeRuntime", "BunRuntime"];
|
|
6
|
-
const isRunnerCall = (node) => effectRunners.has(calleeOf(node, "Effect") ?? "") ||
|
|
7
|
-
runtimes.some((runtime) => calleeOf(node, runtime) === "runMain");
|
|
8
|
-
const isEffectTypeName = (name) => (name.type === "Identifier" && name.name === "Effect") ||
|
|
9
|
-
(name.type === "TSQualifiedName" &&
|
|
10
|
-
name.left.type === "Identifier" &&
|
|
11
|
-
name.left.name === "Effect" &&
|
|
12
|
-
name.right.name === "Effect");
|
|
13
|
-
const isClosedEffectType = (type) => type?.type === "TSTypeReference" &&
|
|
14
|
-
isEffectTypeName(type.typeName) &&
|
|
15
|
-
type.typeArguments?.params[1]?.type === "TSNeverKeyword";
|
|
16
|
-
const parameterType = (identifier) => {
|
|
17
|
-
for (let fn = nearestFunction(identifier); fn; fn = nearestFunction(fn)) {
|
|
18
|
-
for (const param of fn.params) {
|
|
19
|
-
if (param.type === "Identifier" && param.name === identifier.name) {
|
|
20
|
-
return param.typeAnnotation?.typeAnnotation;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return undefined;
|
|
25
|
-
};
|
|
26
|
-
const hasClosedErrorChannel = (node) => {
|
|
27
|
-
if (node?.type === "TSSatisfiesExpression")
|
|
28
|
-
return isClosedEffectType(node.typeAnnotation);
|
|
29
|
-
if (node?.type === "Identifier")
|
|
30
|
-
return isClosedEffectType(parameterType(node));
|
|
31
|
-
return false;
|
|
32
|
-
};
|
|
33
|
-
export const boundaryEntrypointShapeRule = defineRule({
|
|
34
|
-
meta: {
|
|
35
|
-
type: "problem",
|
|
36
|
-
docs: {
|
|
37
|
-
description: "Close the error channel to `never` before running an Effect at the boundary.",
|
|
38
|
-
},
|
|
39
|
-
messages: {
|
|
40
|
-
openErrorChannel: "Close the error channel before running: `program satisfies Effect.Effect<A, never>`, or a runner parameter typed `Effect.Effect<A, never, never>`.",
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
createOnce(context) {
|
|
44
|
-
return {
|
|
45
|
-
CallExpression(node) {
|
|
46
|
-
if (isRunnerCall(node) && !hasClosedErrorChannel(node.arguments[0])) {
|
|
47
|
-
context.report({ node, messageId: "openErrorChannel" });
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const errorHandlingInEffectFnHandlerRule: import("@oxlint/plugins").Rule;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import { isInsideEffectGeneratorBody, memberOf } from "../shared/effect-ast.js";
|
|
3
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
4
|
-
import { isTestFile } from "../shared/test-files.js";
|
|
5
|
-
// mapError, orElse and orElseSucceed stay allowed: they name a failure or give an alternative
|
|
6
|
-
// without opening a handling branch inside the success path.
|
|
7
|
-
const errorHandlers = new Set([
|
|
8
|
-
"catch",
|
|
9
|
-
"catchAll",
|
|
10
|
-
"catchCause",
|
|
11
|
-
"catchCauseFilter",
|
|
12
|
-
"catchCauseIf",
|
|
13
|
-
"catchDefect",
|
|
14
|
-
"catchEager",
|
|
15
|
-
"catchFilter",
|
|
16
|
-
"catchIf",
|
|
17
|
-
"catchReason",
|
|
18
|
-
"catchReasons",
|
|
19
|
-
"catchTag",
|
|
20
|
-
"catchTags",
|
|
21
|
-
"either",
|
|
22
|
-
"exit",
|
|
23
|
-
"match",
|
|
24
|
-
"matchCause",
|
|
25
|
-
"matchCauseEffect",
|
|
26
|
-
"matchEffect",
|
|
27
|
-
"result",
|
|
28
|
-
"sandbox",
|
|
29
|
-
]);
|
|
30
|
-
export const errorHandlingInEffectFnHandlerRule = defineRule({
|
|
31
|
-
meta: {
|
|
32
|
-
type: "suggestion",
|
|
33
|
-
docs: {
|
|
34
|
-
description: "Keep only the success path in Effect generators and handle errors in the second argument of `Effect.fn`. Test files are skipped.",
|
|
35
|
-
},
|
|
36
|
-
messages: {
|
|
37
|
-
handlerInGenerator: "Move `Effect.{{name}}` out of the generator: keep the success path in the body and handle errors in the pipeable arguments of `Effect.fn`.",
|
|
38
|
-
},
|
|
39
|
-
schema: [
|
|
40
|
-
{
|
|
41
|
-
type: "object",
|
|
42
|
-
properties: { testFiles: filePatternsSchema },
|
|
43
|
-
additionalProperties: false,
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
createOnce(context) {
|
|
48
|
-
let inTestFile = false;
|
|
49
|
-
return {
|
|
50
|
-
// In a test the failure is the observed outcome, not a branch of the success path.
|
|
51
|
-
before() {
|
|
52
|
-
inTestFile = isTestFile(context);
|
|
53
|
-
},
|
|
54
|
-
MemberExpression(node) {
|
|
55
|
-
const name = memberOf(node, "Effect");
|
|
56
|
-
if (inTestFile ||
|
|
57
|
-
name === undefined ||
|
|
58
|
-
!errorHandlers.has(name) ||
|
|
59
|
-
!isInsideEffectGeneratorBody(node)) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
context.report({ node, messageId: "handlerInGenerator", data: { name } });
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
},
|
|
66
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const noBareErrorInEffectRule: import("@oxlint/plugins").Rule;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const runOnlyAtEntryRule: import("@oxlint/plugins").Rule;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import { memberOf } from "../shared/effect-ast.js";
|
|
3
|
-
import { isEntryFile } from "../shared/entry-files.js";
|
|
4
|
-
import { filePatternsSchema } from "../shared/file-globs.js";
|
|
5
|
-
const runtimeEntries = [
|
|
6
|
-
["NodeRuntime", "runMain"],
|
|
7
|
-
["BunRuntime", "runMain"],
|
|
8
|
-
["ManagedRuntime", "make"],
|
|
9
|
-
];
|
|
10
|
-
const runnerName = (node) => {
|
|
11
|
-
const effectMember = memberOf(node, "Effect");
|
|
12
|
-
if (effectMember?.startsWith("run"))
|
|
13
|
-
return `Effect.${effectMember}`;
|
|
14
|
-
const entry = runtimeEntries.find(([object, property]) => memberOf(node, object) === property);
|
|
15
|
-
return entry === undefined ? undefined : `${entry[0]}.${entry[1]}`;
|
|
16
|
-
};
|
|
17
|
-
// Reports references too, not only calls: `const run = Effect.runPromise` escapes nothing.
|
|
18
|
-
export const runOnlyAtEntryRule = defineRule({
|
|
19
|
-
meta: {
|
|
20
|
-
type: "problem",
|
|
21
|
-
docs: {
|
|
22
|
-
description: "Run Effect programs only at the entry point. Entry files are skipped.",
|
|
23
|
-
},
|
|
24
|
-
messages: {
|
|
25
|
-
runOutsideEntry: "`{{name}}` runs an Effect program: only the entry point may do that. Tests use `it.effect`, or a single test runner listed in `entryFiles`.",
|
|
26
|
-
},
|
|
27
|
-
schema: [
|
|
28
|
-
{
|
|
29
|
-
type: "object",
|
|
30
|
-
properties: { entryFiles: filePatternsSchema },
|
|
31
|
-
additionalProperties: false,
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
|
-
createOnce(context) {
|
|
36
|
-
let inEntryFile = false;
|
|
37
|
-
return {
|
|
38
|
-
before() {
|
|
39
|
-
inEntryFile = isEntryFile(context);
|
|
40
|
-
},
|
|
41
|
-
MemberExpression(node) {
|
|
42
|
-
if (inEntryFile)
|
|
43
|
-
return;
|
|
44
|
-
const name = runnerName(node);
|
|
45
|
-
if (name !== undefined)
|
|
46
|
-
context.report({ node, messageId: "runOutsideEntry", data: { name } });
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
},
|
|
50
|
-
});
|