oxlint-plugin-effect 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -38
- package/dist/presets/index.d.ts +3 -9
- package/dist/presets/index.js +3 -17
- package/dist/presets/recommended.d.ts +17 -0
- package/dist/presets/recommended.js +17 -0
- package/dist/rules/index.d.ts +4 -61
- package/dist/rules/index.js +4 -118
- package/dist/rules/no-async-function.js +8 -13
- package/dist/rules/no-dynamic-imports.js +89 -60
- package/dist/rules/no-globals.js +164 -127
- package/dist/rules/no-new-error.js +26 -13
- package/dist/rules/no-new-promise.js +22 -21
- package/dist/rules/no-node-builtin-import.js +98 -41
- package/dist/rules/no-throw-statement.js +4 -16
- package/dist/rules/no-try-catch.js +4 -16
- package/package.json +2 -2
- package/dist/presets/core.d.ts +0 -50
- package/dist/presets/core.js +0 -50
- package/dist/presets/effect-native.d.ts +0 -29
- package/dist/presets/effect-native.js +0 -29
- package/dist/presets/full.d.ts +0 -68
- package/dist/presets/full.js +0 -68
- package/dist/presets/functional.d.ts +0 -22
- package/dist/presets/functional.js +0 -22
- package/dist/presets/strict.d.ts +0 -73
- package/dist/presets/strict.js +0 -73
- package/dist/presets/v3-strict.d.ts +0 -16
- package/dist/presets/v3-strict.js +0 -16
- package/dist/presets/v3.d.ts +0 -18
- package/dist/presets/v3.js +0 -18
- package/dist/rules/_effect-context.d.ts +0 -19
- package/dist/rules/_effect-context.js +0 -22
- package/dist/rules/no-arrow-ladder.d.ts +0 -6
- package/dist/rules/no-arrow-ladder.js +0 -61
- package/dist/rules/no-catch-all-to-map-error.d.ts +0 -6
- package/dist/rules/no-catch-all-to-map-error.js +0 -67
- package/dist/rules/no-effect-all-step-sequencing.d.ts +0 -6
- package/dist/rules/no-effect-all-step-sequencing.js +0 -63
- package/dist/rules/no-effect-as.d.ts +0 -6
- package/dist/rules/no-effect-as.js +0 -16
- package/dist/rules/no-effect-async.d.ts +0 -6
- package/dist/rules/no-effect-async.js +0 -16
- package/dist/rules/no-effect-fn-generator.d.ts +0 -6
- package/dist/rules/no-effect-fn-generator.js +0 -30
- package/dist/rules/no-effect-gen-adapter.d.ts +0 -6
- package/dist/rules/no-effect-gen-adapter.js +0 -30
- package/dist/rules/no-effect-map-flatten.d.ts +0 -6
- package/dist/rules/no-effect-map-flatten.js +0 -53
- package/dist/rules/no-effect-map-void.d.ts +0 -6
- package/dist/rules/no-effect-map-void.js +0 -40
- package/dist/rules/no-effect-never.d.ts +0 -6
- package/dist/rules/no-effect-never.js +0 -16
- package/dist/rules/no-effect-orElse-ladder.d.ts +0 -6
- package/dist/rules/no-effect-orElse-ladder.js +0 -53
- package/dist/rules/no-effect-succeed-string.d.ts +0 -6
- package/dist/rules/no-effect-succeed-string.js +0 -30
- package/dist/rules/no-effect-succeed-variable.d.ts +0 -6
- package/dist/rules/no-effect-succeed-variable.js +0 -30
- package/dist/rules/no-effect-succeed-void.d.ts +0 -6
- package/dist/rules/no-effect-succeed-void.js +0 -33
- package/dist/rules/no-effect-sync-console.d.ts +0 -6
- package/dist/rules/no-effect-sync-console.js +0 -63
- package/dist/rules/no-effect-sync-wrapper.d.ts +0 -6
- package/dist/rules/no-effect-sync-wrapper.js +0 -37
- package/dist/rules/no-effect-type-alias.d.ts +0 -6
- package/dist/rules/no-effect-type-alias.js +0 -52
- package/dist/rules/no-effect-wrapper-alias.d.ts +0 -6
- package/dist/rules/no-effect-wrapper-alias.js +0 -69
- package/dist/rules/no-extends-native-error.d.ts +0 -6
- package/dist/rules/no-extends-native-error.js +0 -41
- package/dist/rules/no-flatmap-ladder.d.ts +0 -6
- package/dist/rules/no-flatmap-ladder.js +0 -48
- package/dist/rules/no-fromnullable-coalesce.d.ts +0 -6
- package/dist/rules/no-fromnullable-coalesce.js +0 -36
- package/dist/rules/no-global-error-in-catch.d.ts +0 -6
- package/dist/rules/no-global-error-in-catch.js +0 -75
- package/dist/rules/no-global-error-in-failure.d.ts +0 -6
- package/dist/rules/no-global-error-in-failure.js +0 -54
- package/dist/rules/no-hand-rolled-tagged-union.d.ts +0 -6
- package/dist/rules/no-hand-rolled-tagged-union.js +0 -70
- package/dist/rules/no-if-statement.d.ts +0 -6
- package/dist/rules/no-if-statement.js +0 -18
- package/dist/rules/no-iife-wrapper.d.ts +0 -6
- package/dist/rules/no-iife-wrapper.js +0 -37
- package/dist/rules/no-inline-provide.d.ts +0 -6
- package/dist/rules/no-inline-provide.js +0 -31
- package/dist/rules/no-instanceof-schema.d.ts +0 -6
- package/dist/rules/no-instanceof-schema.js +0 -35
- package/dist/rules/no-make-unsafe.d.ts +0 -6
- package/dist/rules/no-make-unsafe.js +0 -30
- package/dist/rules/no-manual-data-guard.d.ts +0 -6
- package/dist/rules/no-manual-data-guard.js +0 -56
- package/dist/rules/no-manual-effect-channels.d.ts +0 -6
- package/dist/rules/no-manual-effect-channels.js +0 -40
- package/dist/rules/no-match-effect-branch.d.ts +0 -6
- package/dist/rules/no-match-effect-branch.js +0 -64
- package/dist/rules/no-match-void-branch.d.ts +0 -6
- package/dist/rules/no-match-void-branch.js +0 -50
- package/dist/rules/no-multiple-effect-provide.d.ts +0 -6
- package/dist/rules/no-multiple-effect-provide.js +0 -45
- package/dist/rules/no-nested-effect-call.d.ts +0 -6
- package/dist/rules/no-nested-effect-call.js +0 -97
- package/dist/rules/no-nested-effect-gen.d.ts +0 -6
- package/dist/rules/no-nested-effect-gen.js +0 -46
- package/dist/rules/no-nested-pipe.d.ts +0 -6
- package/dist/rules/no-nested-pipe.js +0 -38
- package/dist/rules/no-option-as.d.ts +0 -6
- package/dist/rules/no-option-as.js +0 -16
- package/dist/rules/no-option-boolean-normalization.d.ts +0 -6
- package/dist/rules/no-option-boolean-normalization.js +0 -50
- package/dist/rules/no-positional-log-error.d.ts +0 -6
- package/dist/rules/no-positional-log-error.js +0 -52
- package/dist/rules/no-promise-control-flow-in-tests.d.ts +0 -6
- package/dist/rules/no-promise-control-flow-in-tests.js +0 -94
- package/dist/rules/no-redundant-schema-tag-identifier.d.ts +0 -6
- package/dist/rules/no-redundant-schema-tag-identifier.js +0 -46
- package/dist/rules/no-return-in-arrow.d.ts +0 -6
- package/dist/rules/no-return-in-arrow.js +0 -41
- package/dist/rules/no-return-nullish.d.ts +0 -6
- package/dist/rules/no-return-nullish.js +0 -47
- package/dist/rules/no-run-in-effect-gen.d.ts +0 -6
- package/dist/rules/no-run-in-effect-gen.js +0 -48
- package/dist/rules/no-runtime-run-fork.d.ts +0 -6
- package/dist/rules/no-runtime-run-fork.js +0 -19
- package/dist/rules/no-schema-struct-with-tag.d.ts +0 -6
- package/dist/rules/no-schema-struct-with-tag.js +0 -48
- package/dist/rules/no-schema-struct.d.ts +0 -6
- package/dist/rules/no-schema-struct.js +0 -28
- package/dist/rules/no-schema-union-of-literals.d.ts +0 -6
- package/dist/rules/no-schema-union-of-literals.js +0 -35
- package/dist/rules/no-sleep-in-tests.d.ts +0 -6
- package/dist/rules/no-sleep-in-tests.js +0 -55
- package/dist/rules/no-spread.d.ts +0 -6
- package/dist/rules/no-spread.js +0 -26
- package/dist/rules/no-string-sentinel-const.d.ts +0 -6
- package/dist/rules/no-string-sentinel-const.js +0 -38
- package/dist/rules/no-switch-statement.d.ts +0 -6
- package/dist/rules/no-switch-statement.js +0 -16
- package/dist/rules/no-unnecessary-arrow-block.d.ts +0 -6
- package/dist/rules/no-unnecessary-arrow-block.js +0 -34
- package/dist/rules/no-unnecessary-effect-gen.d.ts +0 -6
- package/dist/rules/no-unnecessary-effect-gen.js +0 -35
- package/dist/rules/no-unnecessary-pipe-chain.d.ts +0 -6
- package/dist/rules/no-unnecessary-pipe-chain.js +0 -36
- package/dist/rules/no-unnecessary-pipe.d.ts +0 -6
- package/dist/rules/no-unnecessary-pipe.js +0 -29
|
@@ -1,87 +1,116 @@
|
|
|
1
|
+
import { importSource, narrow } from "../vendor/effect-oxlint/AST.js";
|
|
1
2
|
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
3
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
4
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
5
|
import "../vendor/effect-oxlint/index.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
5
7
|
import * as Effect from "effect/Effect";
|
|
6
8
|
//#region src/rules/no-dynamic-imports.ts
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return
|
|
9
|
+
const identifierName = (node) => node?.type === "Identifier" ? node.name : void 0;
|
|
10
|
+
const hasNamedBinding = (pattern) => {
|
|
11
|
+
if (pattern.type === "Identifier") return true;
|
|
12
|
+
if (pattern.type === "ObjectPattern") return pattern.properties.length > 0;
|
|
13
|
+
if (pattern.type === "ArrayPattern") return pattern.elements.some((element) => element !== null);
|
|
14
|
+
return false;
|
|
12
15
|
};
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
const isNamedVariable = (node, value) => node.type === "VariableDeclarator" && node.init === value && hasNamedBinding(node.id);
|
|
17
|
+
const transparentWrappers = new Set([
|
|
18
|
+
"AwaitExpression",
|
|
19
|
+
"ChainExpression",
|
|
20
|
+
"TSAsExpression",
|
|
21
|
+
"TSNonNullExpression",
|
|
22
|
+
"TSTypeAssertion",
|
|
23
|
+
"YieldExpression"
|
|
24
|
+
]);
|
|
25
|
+
const climbTransparent = (start) => {
|
|
26
|
+
let current = start;
|
|
27
|
+
while (current.parent != null && transparentWrappers.has(current.parent.type)) current = current.parent;
|
|
28
|
+
return current;
|
|
16
29
|
};
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const point = loc[edge];
|
|
21
|
-
if (typeof point !== "object" || point === null) return void 0;
|
|
22
|
-
const line = point["line"];
|
|
23
|
-
return typeof line === "number" ? line : void 0;
|
|
30
|
+
const isNamedDirectBinding = (node) => {
|
|
31
|
+
const value = climbTransparent(node);
|
|
32
|
+
return value.parent != null && isNamedVariable(value.parent, value);
|
|
24
33
|
};
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return allowPattern.test(comment.value);
|
|
33
|
-
});
|
|
34
|
+
const isNamedFunctionBoundary = (node) => {
|
|
35
|
+
const parent = node.parent;
|
|
36
|
+
if (parent?.type === "ArrowFunctionExpression" && parent.body === node) return parent.parent != null && isNamedVariable(parent.parent, parent);
|
|
37
|
+
if (parent?.type !== "ReturnStatement" || parent.argument !== node) return false;
|
|
38
|
+
const block = parent.parent;
|
|
39
|
+
const fn = block?.parent;
|
|
40
|
+
return block?.type === "BlockStatement" && fn?.type === "FunctionDeclaration" && fn.id !== null;
|
|
34
41
|
};
|
|
35
|
-
const
|
|
36
|
-
if (node.type !== "
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const callee = getNodeField(init, "callee");
|
|
41
|
-
if (callee?.type !== "Identifier" || getStringField(callee, "name") !== "createRequire") return void 0;
|
|
42
|
-
return getStringField(id, "name");
|
|
42
|
+
const isEffectPromiseCall = (node) => {
|
|
43
|
+
if (node.type !== "CallExpression") return false;
|
|
44
|
+
const callee = node.callee;
|
|
45
|
+
if (callee.type !== "MemberExpression" || callee.computed) return false;
|
|
46
|
+
return identifierName(callee.object) === "Effect" && ["promise", "tryPromise"].includes(identifierName(callee.property) ?? "");
|
|
43
47
|
};
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const inner = getNodeField(callee, "callee");
|
|
58
|
-
if (inner?.type === "Identifier" && getStringField(inner, "name") === "createRequire") return "`createRequire(...)(...)` is forbidden. Use a top-level static import.";
|
|
59
|
-
}
|
|
48
|
+
const isEffectPromiseBoundary = (node) => {
|
|
49
|
+
const callback = node.parent;
|
|
50
|
+
if (callback?.type !== "ArrowFunctionExpression" || callback.body !== node) return false;
|
|
51
|
+
const call = callback.parent;
|
|
52
|
+
return call != null && isEffectPromiseCall(call);
|
|
53
|
+
};
|
|
54
|
+
const isNamedLazyBoundary = (node) => {
|
|
55
|
+
return isNamedDirectBinding(node) || isNamedFunctionBoundary(node) || isEffectPromiseBoundary(node);
|
|
56
|
+
};
|
|
57
|
+
const dynamicRequireMessage = (callee) => {
|
|
58
|
+
if (identifierName(callee) === "require") return "Avoid require(). Use a static import.";
|
|
59
|
+
if (callee?.type !== "MemberExpression") return void 0;
|
|
60
|
+
return identifierName(callee.object) === "module" && identifierName(callee.property) === "require" ? "Avoid module.require(). Use a static import." : void 0;
|
|
60
61
|
};
|
|
61
62
|
const noDynamicImports = define({
|
|
62
63
|
name: "no-dynamic-imports",
|
|
63
64
|
meta: meta({
|
|
64
65
|
type: "problem",
|
|
65
|
-
description: "
|
|
66
|
+
description: "Keep dynamic imports behind named lazy-loading boundaries."
|
|
66
67
|
}),
|
|
67
68
|
create: function* () {
|
|
68
69
|
const ctx = yield* RuleContext;
|
|
69
|
-
const
|
|
70
|
-
const
|
|
70
|
+
const createRequireNames = /* @__PURE__ */ new Set();
|
|
71
|
+
const requireAliases = /* @__PURE__ */ new Set();
|
|
72
|
+
const report = (node, message) => ctx.report(make({
|
|
71
73
|
node,
|
|
72
74
|
message
|
|
73
75
|
}));
|
|
74
76
|
return {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
ImportDeclaration: (node) => Option.match(narrow(node, "ImportDeclaration"), {
|
|
78
|
+
onNone: () => Effect.void,
|
|
79
|
+
onSome: (declaration) => {
|
|
80
|
+
if (!["module", "node:module"].includes(importSource(declaration))) return Effect.void;
|
|
81
|
+
for (const specifier of declaration.specifiers) if (specifier.type === "ImportSpecifier" && identifierName(specifier.imported) === "createRequire") createRequireNames.add(specifier.local.name);
|
|
82
|
+
return Effect.void;
|
|
83
|
+
}
|
|
84
|
+
}),
|
|
85
|
+
VariableDeclarator: (node) => Option.match(narrow(node, "VariableDeclarator"), {
|
|
86
|
+
onNone: () => Effect.void,
|
|
87
|
+
onSome: (declaration) => {
|
|
88
|
+
if (declaration.id.type === "Identifier" && declaration.init?.type === "CallExpression" && declaration.init.callee.type === "Identifier" && createRequireNames.has(declaration.init.callee.name)) {
|
|
89
|
+
requireAliases.add(declaration.id.name);
|
|
90
|
+
return report(declaration, "Avoid createRequire(). Keep module loading static or use a named import() boundary.");
|
|
91
|
+
}
|
|
92
|
+
return Effect.void;
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
ImportExpression: (node) => Option.match(narrow(node, "ImportExpression"), {
|
|
96
|
+
onNone: () => Effect.void,
|
|
97
|
+
onSome: (importExpression) => isNamedLazyBoundary(importExpression) ? Effect.void : ctx.report(make({
|
|
98
|
+
node,
|
|
99
|
+
message: "Avoid inline dynamic imports. Bind the imported module or a lazy loader to a descriptive name before using it."
|
|
100
|
+
}))
|
|
101
|
+
}),
|
|
82
102
|
CallExpression: (node) => {
|
|
83
|
-
|
|
84
|
-
|
|
103
|
+
return Option.match(narrow(node, "CallExpression"), {
|
|
104
|
+
onNone: () => Effect.void,
|
|
105
|
+
onSome: (call) => {
|
|
106
|
+
const calleeName = identifierName(call.callee);
|
|
107
|
+
const message = calleeName !== void 0 && requireAliases.has(calleeName) ? "Avoid createRequire aliases. Keep module loading static." : dynamicRequireMessage(call.callee);
|
|
108
|
+
return message === void 0 ? Effect.void : ctx.report(make({
|
|
109
|
+
node: call,
|
|
110
|
+
message
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
85
114
|
}
|
|
86
115
|
};
|
|
87
116
|
}
|
package/dist/rules/no-globals.js
CHANGED
|
@@ -1,163 +1,200 @@
|
|
|
1
|
-
import { calleeName,
|
|
1
|
+
import { calleeName, memberNames, narrow } from "../vendor/effect-oxlint/AST.js";
|
|
2
2
|
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
3
3
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
4
|
-
import { merge } from "../vendor/effect-oxlint/Visitor.js";
|
|
5
4
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
5
|
+
import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
|
|
6
6
|
import "../vendor/effect-oxlint/index.js";
|
|
7
|
-
import { makeEffectContextTracker } from "./_effect-context.js";
|
|
8
7
|
import * as Option from "effect/Option";
|
|
9
8
|
import * as Effect from "effect/Effect";
|
|
10
|
-
import * as Ref from "effect/Ref";
|
|
11
9
|
//#region src/rules/no-globals.ts
|
|
12
10
|
const memberBans = [
|
|
13
11
|
[
|
|
14
12
|
"console",
|
|
15
|
-
[
|
|
16
|
-
"
|
|
17
|
-
"warn",
|
|
13
|
+
new Set([
|
|
14
|
+
"debug",
|
|
18
15
|
"error",
|
|
19
16
|
"info",
|
|
20
|
-
"
|
|
21
|
-
"trace"
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
"log",
|
|
18
|
+
"trace",
|
|
19
|
+
"warn"
|
|
20
|
+
]),
|
|
21
|
+
"Effect logging or Console"
|
|
24
22
|
],
|
|
25
23
|
[
|
|
26
24
|
"Date",
|
|
27
|
-
"now",
|
|
28
|
-
"
|
|
25
|
+
new Set(["now"]),
|
|
26
|
+
"Clock or DateTime"
|
|
29
27
|
],
|
|
30
28
|
[
|
|
31
|
-
"
|
|
32
|
-
["
|
|
33
|
-
"
|
|
29
|
+
"Math",
|
|
30
|
+
new Set(["random"]),
|
|
31
|
+
"Random"
|
|
34
32
|
],
|
|
35
33
|
[
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
34
|
+
"performance",
|
|
35
|
+
new Set(["now"]),
|
|
36
|
+
"Clock.currentTimeNanos"
|
|
39
37
|
],
|
|
40
38
|
[
|
|
41
39
|
"crypto",
|
|
42
|
-
["
|
|
43
|
-
"
|
|
40
|
+
new Set(["getRandomValues", "randomUUID"]),
|
|
41
|
+
"Crypto"
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"JSON",
|
|
45
|
+
new Set(["parse", "stringify"]),
|
|
46
|
+
"Schema JSON codecs"
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"process",
|
|
50
|
+
new Set([
|
|
51
|
+
"argv",
|
|
52
|
+
"chdir",
|
|
53
|
+
"env",
|
|
54
|
+
"exit",
|
|
55
|
+
"hrtime",
|
|
56
|
+
"nextTick",
|
|
57
|
+
"stderr",
|
|
58
|
+
"stdin",
|
|
59
|
+
"stdout"
|
|
60
|
+
]),
|
|
61
|
+
"Config, Stdio, Clock, or Effect scheduling"
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"Bun",
|
|
65
|
+
new Set([
|
|
66
|
+
"$",
|
|
67
|
+
"Glob",
|
|
68
|
+
"connect",
|
|
69
|
+
"env",
|
|
70
|
+
"file",
|
|
71
|
+
"listen",
|
|
72
|
+
"nanoseconds",
|
|
73
|
+
"randomUUIDv7",
|
|
74
|
+
"redis",
|
|
75
|
+
"serve",
|
|
76
|
+
"sleep",
|
|
77
|
+
"spawn",
|
|
78
|
+
"spawnSync",
|
|
79
|
+
"stderr",
|
|
80
|
+
"stdin",
|
|
81
|
+
"stdout",
|
|
82
|
+
"write"
|
|
83
|
+
]),
|
|
84
|
+
"the corresponding Effect platform service"
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"Deno",
|
|
88
|
+
new Set([
|
|
89
|
+
"args",
|
|
90
|
+
"env",
|
|
91
|
+
"exit",
|
|
92
|
+
"mkdir",
|
|
93
|
+
"open",
|
|
94
|
+
"readDir",
|
|
95
|
+
"readFile",
|
|
96
|
+
"readTextFile",
|
|
97
|
+
"remove",
|
|
98
|
+
"serve",
|
|
99
|
+
"stat",
|
|
100
|
+
"stderr",
|
|
101
|
+
"stdin",
|
|
102
|
+
"stdout",
|
|
103
|
+
"writeFile",
|
|
104
|
+
"writeTextFile"
|
|
105
|
+
]),
|
|
106
|
+
"the corresponding Effect platform service"
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
"localStorage",
|
|
110
|
+
new Set([
|
|
111
|
+
"clear",
|
|
112
|
+
"getItem",
|
|
113
|
+
"key",
|
|
114
|
+
"removeItem",
|
|
115
|
+
"setItem"
|
|
116
|
+
]),
|
|
117
|
+
"KeyValueStore"
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"sessionStorage",
|
|
121
|
+
new Set([
|
|
122
|
+
"clear",
|
|
123
|
+
"getItem",
|
|
124
|
+
"key",
|
|
125
|
+
"removeItem",
|
|
126
|
+
"setItem"
|
|
127
|
+
]),
|
|
128
|
+
"KeyValueStore"
|
|
44
129
|
]
|
|
45
130
|
];
|
|
46
|
-
const
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
131
|
+
const callBans = new Map([
|
|
132
|
+
["atob", "Encoding.decodeBase64"],
|
|
133
|
+
["btoa", "Encoding.encodeBase64"],
|
|
134
|
+
["fetch", "HttpClient"],
|
|
135
|
+
["queueMicrotask", "Effect scheduling"],
|
|
136
|
+
["setImmediate", "Effect scheduling"],
|
|
137
|
+
["setInterval", "Effect.sleep with Schedule"],
|
|
138
|
+
["setTimeout", "Effect.sleep or Schedule"]
|
|
50
139
|
]);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
randomUUID: "Use Random service from 'effect'",
|
|
68
|
-
randomUUIDv7: "Use Random service from 'effect'"
|
|
69
|
-
},
|
|
70
|
-
process: {
|
|
71
|
-
env: "Use Config from 'effect'",
|
|
72
|
-
exit: "Use Effect.fail or Effect.die",
|
|
73
|
-
cwd: "Use Path from 'effect'",
|
|
74
|
-
argv: "Use Args/Command from 'effect/unstable/cli'",
|
|
75
|
-
stdin: "Use Terminal from 'effect'",
|
|
76
|
-
stdout: "Use Terminal from 'effect'",
|
|
77
|
-
stderr: "Use Terminal from 'effect'",
|
|
78
|
-
chdir: "Use Path from 'effect'",
|
|
79
|
-
kill: "Use Command from 'effect'",
|
|
80
|
-
on: "Use Effect.async or Stream.async",
|
|
81
|
-
platform: "Use PlatformInfo from 'effect'",
|
|
82
|
-
arch: "Use PlatformInfo from 'effect'",
|
|
83
|
-
execPath: "Use Command from 'effect'",
|
|
84
|
-
execArgv: "Use Args/Command from 'effect/unstable/cli'"
|
|
85
|
-
},
|
|
86
|
-
Deno: {
|
|
87
|
-
readFile: "Use FileSystem from 'effect'",
|
|
88
|
-
writeFile: "Use FileSystem from 'effect'",
|
|
89
|
-
readTextFile: "Use FileSystem from 'effect'",
|
|
90
|
-
writeTextFile: "Use FileSystem from 'effect'",
|
|
91
|
-
open: "Use FileSystem from 'effect'",
|
|
92
|
-
remove: "Use FileSystem from 'effect'",
|
|
93
|
-
mkdir: "Use FileSystem from 'effect'",
|
|
94
|
-
readDir: "Use FileSystem from 'effect'",
|
|
95
|
-
stat: "Use FileSystem from 'effect'",
|
|
96
|
-
cwd: "Use Path from 'effect'",
|
|
97
|
-
env: "Use Config from 'effect'",
|
|
98
|
-
exit: "Use Effect.fail or Effect.die",
|
|
99
|
-
serve: "Use HttpServer from 'effect/unstable/http'",
|
|
100
|
-
run: "Use Command from 'effect'",
|
|
101
|
-
args: "Use Args/Command from 'effect/unstable/cli'"
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
function getHint(obj, prop) {
|
|
105
|
-
const hint = alternatives[obj]?.[prop];
|
|
106
|
-
if (hint) return `. ${hint}`;
|
|
107
|
-
return ". Wrap in a service for testability";
|
|
108
|
-
}
|
|
109
|
-
const callBans = {
|
|
110
|
-
fetch: "Avoid fetch() inside Effect context. Use HttpClient from 'effect/unstable/http'.",
|
|
111
|
-
setTimeout: "Avoid setTimeout inside Effect context. Use Effect.sleep or Schedule.",
|
|
112
|
-
setInterval: "Avoid setInterval inside Effect context. Use Effect.sleep or Schedule."
|
|
140
|
+
const constructorBans = new Map([
|
|
141
|
+
["Date", "Clock or DateTime"],
|
|
142
|
+
["SharedWorker", "Effect Worker"],
|
|
143
|
+
["WebSocket", "Socket"],
|
|
144
|
+
["Worker", "Effect Worker"]
|
|
145
|
+
]);
|
|
146
|
+
const isUnshadowedGlobal = (ctx, node, name) => Option.match(findVariableUp(ctx.sourceCode.getScope(node), name), {
|
|
147
|
+
onNone: () => true,
|
|
148
|
+
onSome: (variable) => variable.defs.length === 0
|
|
149
|
+
});
|
|
150
|
+
const staticMember = (node) => Option.getOrUndefined(memberNames(node));
|
|
151
|
+
const isCryptoDigest = (node) => {
|
|
152
|
+
if (node.computed || node.property.type !== "Identifier" || node.property.name !== "digest") return false;
|
|
153
|
+
const object = node.object;
|
|
154
|
+
if (object.type !== "MemberExpression" || object.computed) return false;
|
|
155
|
+
return object.object.type === "Identifier" && object.object.name === "crypto" && object.property.type === "Identifier" && object.property.name === "subtle";
|
|
113
156
|
};
|
|
114
157
|
const noGlobals = define({
|
|
115
158
|
name: "no-globals",
|
|
116
159
|
meta: meta({
|
|
117
|
-
type: "
|
|
118
|
-
description: "Avoid
|
|
160
|
+
type: "problem",
|
|
161
|
+
description: "Avoid ambient runtime capabilities that Effect provides as services."
|
|
119
162
|
}),
|
|
120
163
|
create: function* () {
|
|
121
164
|
const ctx = yield* RuleContext;
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const names =
|
|
132
|
-
if (names
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
message: `Avoid ${obj}.${prop} inside Effect context${getHint(obj, prop)}.`
|
|
137
|
-
}));
|
|
138
|
-
}
|
|
139
|
-
return Effect.void;
|
|
140
|
-
}),
|
|
141
|
-
CallExpression: (node) => Effect.flatMap(Ref.get(depth), (d) => {
|
|
142
|
-
if (d <= 0) return Effect.void;
|
|
143
|
-
const name = Option.getOrUndefined(calleeName(node));
|
|
144
|
-
const message = name !== void 0 ? callBans[name] : void 0;
|
|
145
|
-
if (message !== void 0) return ctx.report(make({
|
|
146
|
-
node,
|
|
147
|
-
message
|
|
148
|
-
}));
|
|
149
|
-
return Effect.void;
|
|
150
|
-
}),
|
|
151
|
-
NewExpression: (node) => Effect.flatMap(Ref.get(depth), (d) => {
|
|
152
|
-
if (d <= 0) return Effect.void;
|
|
153
|
-
const callee = node["callee"];
|
|
154
|
-
if (callee.type === "Identifier" && "name" in callee && callee.name === "Date") return ctx.report(make({
|
|
155
|
-
node,
|
|
156
|
-
message: "Avoid new Date() inside Effect context. Use Clock service from 'effect'."
|
|
157
|
-
}));
|
|
165
|
+
const report = (node, used, alternative) => ctx.report(make({
|
|
166
|
+
node,
|
|
167
|
+
message: `Avoid ${used}. Use ${alternative}; platform adapters may disable this rule explicitly.`
|
|
168
|
+
}));
|
|
169
|
+
return {
|
|
170
|
+
MemberExpression: (node) => {
|
|
171
|
+
const memberExpression = Option.getOrUndefined(narrow(node, "MemberExpression"));
|
|
172
|
+
if (memberExpression === void 0) return Effect.void;
|
|
173
|
+
if (isCryptoDigest(memberExpression) && isUnshadowedGlobal(ctx, memberExpression, "crypto")) return report(memberExpression, "crypto.subtle.digest", "Crypto.digest");
|
|
174
|
+
const names = staticMember(memberExpression);
|
|
175
|
+
if (names === void 0) return Effect.void;
|
|
176
|
+
const [object, property] = names;
|
|
177
|
+
if (!isUnshadowedGlobal(ctx, memberExpression, object)) return Effect.void;
|
|
178
|
+
for (const [bannedObject, properties, alternative] of memberBans) if (object === bannedObject && properties.has(property)) return report(memberExpression, `${object}.${property}`, alternative);
|
|
158
179
|
return Effect.void;
|
|
159
|
-
}
|
|
160
|
-
|
|
180
|
+
},
|
|
181
|
+
CallExpression: (node) => {
|
|
182
|
+
const call = Option.getOrUndefined(narrow(node, "CallExpression"));
|
|
183
|
+
if (call === void 0) return Effect.void;
|
|
184
|
+
const name = Option.getOrUndefined(calleeName(call));
|
|
185
|
+
if (name === void 0 || !isUnshadowedGlobal(ctx, call, name)) return Effect.void;
|
|
186
|
+
const alternative = callBans.get(name);
|
|
187
|
+
return alternative === void 0 ? Effect.void : report(call, `${name}()`, alternative);
|
|
188
|
+
},
|
|
189
|
+
NewExpression: (node) => {
|
|
190
|
+
const expression = Option.getOrUndefined(narrow(node, "NewExpression"));
|
|
191
|
+
if (expression === void 0 || expression.callee.type !== "Identifier") return Effect.void;
|
|
192
|
+
const name = expression.callee.name;
|
|
193
|
+
if (!isUnshadowedGlobal(ctx, expression, name)) return Effect.void;
|
|
194
|
+
const alternative = constructorBans.get(name);
|
|
195
|
+
return alternative === void 0 ? Effect.void : report(expression, `new ${name}()`, alternative);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
161
198
|
}
|
|
162
199
|
});
|
|
163
200
|
//#endregion
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { narrow } from "../vendor/effect-oxlint/AST.js";
|
|
1
2
|
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
3
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
-
import { merge } from "../vendor/effect-oxlint/Visitor.js";
|
|
4
4
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
5
5
|
import "../vendor/effect-oxlint/index.js";
|
|
6
|
-
import
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
|
-
import * as Ref from "effect/Ref";
|
|
9
8
|
//#region src/rules/no-new-error.ts
|
|
10
9
|
const nativeErrors = new Set([
|
|
11
10
|
"Error",
|
|
@@ -16,23 +15,37 @@ const nativeErrors = new Set([
|
|
|
16
15
|
"URIError",
|
|
17
16
|
"EvalError"
|
|
18
17
|
]);
|
|
18
|
+
const defectNamespaces = new Set([
|
|
19
|
+
"Effect",
|
|
20
|
+
"Cause",
|
|
21
|
+
"Exit"
|
|
22
|
+
]);
|
|
23
|
+
const isDirectDefectArgument = (node) => {
|
|
24
|
+
const parent = node.parent;
|
|
25
|
+
if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return false;
|
|
26
|
+
const callee = parent.callee;
|
|
27
|
+
if (callee.type !== "MemberExpression" || callee.computed) return false;
|
|
28
|
+
if (callee.object.type !== "Identifier" || callee.property.type !== "Identifier") return false;
|
|
29
|
+
return defectNamespaces.has(callee.object.name) && callee.property.name === "die";
|
|
30
|
+
};
|
|
19
31
|
const noNewError = define({
|
|
20
32
|
name: "no-new-error",
|
|
21
33
|
meta: meta({
|
|
22
34
|
type: "suggestion",
|
|
23
|
-
description: "
|
|
35
|
+
description: "Reserve native Error values for explicit Effect defects."
|
|
24
36
|
}),
|
|
25
37
|
create: function* () {
|
|
26
38
|
const ctx = yield* RuleContext;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
return { NewExpression: (node) => Option.match(narrow(node, "NewExpression"), {
|
|
40
|
+
onNone: () => Effect.void,
|
|
41
|
+
onSome: (expression) => {
|
|
42
|
+
if (expression.callee.type !== "Identifier" || !nativeErrors.has(expression.callee.name) || isDirectDefectArgument(expression)) return Effect.void;
|
|
43
|
+
return ctx.report(make({
|
|
44
|
+
node: expression,
|
|
45
|
+
message: "Avoid native Error constructors for expected failures. Use a tagged error, or pass the Error directly to Effect.die, Cause.die, or Exit.die for an explicit defect."
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}) };
|
|
36
49
|
}
|
|
37
50
|
});
|
|
38
51
|
//#endregion
|
|
@@ -1,37 +1,38 @@
|
|
|
1
|
+
import { memberNames, narrow } from "../vendor/effect-oxlint/AST.js";
|
|
1
2
|
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
3
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
-
import { merge } from "../vendor/effect-oxlint/Visitor.js";
|
|
4
4
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
5
5
|
import "../vendor/effect-oxlint/index.js";
|
|
6
|
-
import
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
|
-
import * as Ref from "effect/Ref";
|
|
9
8
|
//#region src/rules/no-new-promise.ts
|
|
10
|
-
|
|
11
|
-
* Ban `new Promise()`. Context-aware messaging.
|
|
12
|
-
*
|
|
13
|
-
* Inside Effect.gen/fn: "Use Effect.tryPromise or Effect.async"
|
|
14
|
-
* Outside: "Use Effect.tryPromise or wrap with Effect.fn"
|
|
15
|
-
*
|
|
16
|
-
* Source: language-service/newPromise
|
|
17
|
-
*/
|
|
9
|
+
const isPromiseIdentifier = (node) => node.type === "Identifier" && "name" in node && node.name === "Promise";
|
|
18
10
|
const noNewPromise = define({
|
|
19
11
|
name: "no-new-promise",
|
|
20
12
|
meta: meta({
|
|
21
13
|
type: "suggestion",
|
|
22
|
-
description: "Avoid
|
|
14
|
+
description: "Avoid Promise APIs. Use Effect concurrency and promise boundaries."
|
|
23
15
|
}),
|
|
24
16
|
create: function* () {
|
|
25
17
|
const ctx = yield* RuleContext;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
const report = (node) => ctx.report(make({
|
|
19
|
+
node,
|
|
20
|
+
message: "Avoid Promise APIs. Use Effect.async for callbacks and Effect.promise or Effect.tryPromise at promise boundaries."
|
|
21
|
+
}));
|
|
22
|
+
return {
|
|
23
|
+
NewExpression: (node) => Option.match(narrow(node, "NewExpression"), {
|
|
24
|
+
onNone: () => Effect.void,
|
|
25
|
+
onSome: (expression) => isPromiseIdentifier(expression.callee) ? report(expression) : Effect.void
|
|
26
|
+
}),
|
|
27
|
+
CallExpression: (node) => Option.match(narrow(node, "CallExpression"), {
|
|
28
|
+
onNone: () => Effect.void,
|
|
29
|
+
onSome: (call) => {
|
|
30
|
+
if (isPromiseIdentifier(call.callee)) return report(call);
|
|
31
|
+
if (call.callee.type !== "MemberExpression") return Effect.void;
|
|
32
|
+
return Option.getOrUndefined(memberNames(call.callee))?.[0] === "Promise" ? report(call) : Effect.void;
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
};
|
|
35
36
|
}
|
|
36
37
|
});
|
|
37
38
|
//#endregion
|