oxlint-plugin-effect 0.10.0 → 0.12.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 +58 -22
- package/dist/presets/recommended.d.ts +20 -0
- package/dist/presets/recommended.js +15 -1
- package/dist/rules/_effect-context.js +27 -0
- package/dist/rules/_effect-namespaces.js +25 -0
- package/dist/rules/_function-metrics.js +283 -0
- package/dist/rules/_max-option.js +19 -0
- package/dist/rules/_tagged-control-flow.js +1 -1
- package/dist/rules/index.d.ts +14 -1
- package/dist/rules/index.js +27 -1
- package/dist/rules/max-cognitive-complexity.d.ts +6 -0
- package/dist/rules/max-cognitive-complexity.js +43 -0
- package/dist/rules/max-halstead-difficulty.d.ts +6 -0
- package/dist/rules/max-halstead-difficulty.js +44 -0
- package/dist/rules/no-inline-provide.d.ts +6 -0
- package/dist/rules/no-inline-provide.js +39 -0
- package/dist/rules/no-managed-runtime-in-effect.d.ts +6 -0
- package/dist/rules/no-managed-runtime-in-effect.js +41 -0
- package/dist/rules/no-nested-effect-gen.d.ts +6 -0
- package/dist/rules/no-nested-effect-gen.js +39 -0
- package/dist/rules/no-per-call-cache-construction.d.ts +6 -0
- package/dist/rules/no-per-call-cache-construction.js +52 -0
- package/dist/rules/no-run-collect-on-unbounded-stream.d.ts +6 -0
- package/dist/rules/no-run-collect-on-unbounded-stream.js +81 -0
- package/dist/rules/no-sequential-effect-all.d.ts +6 -0
- package/dist/rules/no-sequential-effect-all.js +61 -0
- package/dist/rules/no-silent-catch-all.d.ts +6 -0
- package/dist/rules/no-silent-catch-all.js +51 -0
- package/dist/rules/no-unbounded-concurrency.d.ts +6 -0
- package/dist/rules/no-unbounded-concurrency.js +50 -0
- package/dist/rules/no-unbounded-retry.d.ts +6 -0
- package/dist/rules/no-unbounded-retry.js +114 -0
- package/dist/rules/prefer-catch-tag.js +33 -6
- package/dist/rules/prefer-match-tags-exhaustive.js +62 -8
- package/dist/rules/prefer-service-of.d.ts +6 -0
- package/dist/rules/prefer-service-of.js +65 -0
- package/dist/rules/require-named-effect-fn.d.ts +6 -0
- package/dist/rules/require-named-effect-fn.js +37 -0
- package/dist/vendor/effect-oxlint/Rule.d.ts +11 -1
- package/dist/vendor/effect-oxlint/Rule.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,31 +21,42 @@ export default {
|
|
|
21
21
|
};
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
If the project uses JSON configuration, copy the exported rule map into `rules`; every recommended rule has `error` severity.
|
|
24
|
+
If the project uses JSON configuration, copy the exported rule map into `rules`; every recommended rule has `error` severity. The complexity rules carry their limit as an option, `["error", { "max": 21 }]`.
|
|
25
25
|
|
|
26
26
|
## Recommended Rules
|
|
27
27
|
|
|
28
|
-
| Rule
|
|
29
|
-
|
|
|
30
|
-
| `effect/noAs`
|
|
31
|
-
| `effect/noAsyncFunction`
|
|
32
|
-
| `effect/noTryCatch`
|
|
33
|
-
| `effect/noTestLifecycleHooks`
|
|
34
|
-
| `effect/noThrowStatement`
|
|
35
|
-
| `effect/noNewPromise`
|
|
36
|
-
| `effect/noNewError`
|
|
37
|
-
| `effect/noNullish`
|
|
38
|
-
| `effect/noModuleMocks`
|
|
39
|
-
| `effect/noTernary`
|
|
40
|
-
| `effect/
|
|
41
|
-
| `effect/
|
|
42
|
-
| `effect/
|
|
43
|
-
| `effect/
|
|
44
|
-
| `effect/
|
|
45
|
-
| `effect/
|
|
46
|
-
| `effect/
|
|
47
|
-
| `effect/
|
|
48
|
-
| `effect/
|
|
28
|
+
| Rule | Contract |
|
|
29
|
+
| -------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
30
|
+
| `effect/noAs` | Bans TypeScript `as` assertions; use `satisfies` |
|
|
31
|
+
| `effect/noAsyncFunction` | Bans async functions and await expressions |
|
|
32
|
+
| `effect/noTryCatch` | Bans every try/catch/finally statement |
|
|
33
|
+
| `effect/noTestLifecycleHooks` | Bans `beforeEach`, `afterEach`, `beforeAll`, and `afterAll`; use Effect scopes instead |
|
|
34
|
+
| `effect/noThrowStatement` | Bans every throw statement |
|
|
35
|
+
| `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
|
|
36
|
+
| `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
|
|
37
|
+
| `effect/noNullish` | Bans null and undefined; use Option or a domain enum for richer state |
|
|
38
|
+
| `effect/noModuleMocks` | Bans Vitest and Jest module mocks and method spies; use Effect service test layers |
|
|
39
|
+
| `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
|
|
40
|
+
| `effect/noManagedRuntimeInEffect` | Keeps ManagedRuntime construction at non-Effect host boundaries |
|
|
41
|
+
| `effect/noInlineProvide` | Keeps dependency provisioning at explicit composition boundaries |
|
|
42
|
+
| `effect/noNestedEffectGen` | Flattens directly yielded nested generators |
|
|
43
|
+
| `effect/noPerCallCacheConstruction` | Constructs shared caches once in their owning layer |
|
|
44
|
+
| `effect/noRunCollectOnUnboundedStream` | Requires termination before collecting a clearly unbounded Stream |
|
|
45
|
+
| `effect/noSequentialEffectAll` | Uses explicit sequencing when serial aggregation discards its result |
|
|
46
|
+
| `effect/noSilentCatchAll` | Keeps swallowed failures visible or recovers them truthfully |
|
|
47
|
+
| `effect/noUnboundedConcurrency` | Requires finite concurrency for collections that can grow |
|
|
48
|
+
| `effect/noUnboundedRetry` | Requires an attempt or duration bound on retry schedules |
|
|
49
|
+
| `effect/noDynamicImports` | Allows import() only behind a named lazy-loading boundary; bans require() |
|
|
50
|
+
| `effect/noEffectDo` | Bans Effect.Do |
|
|
51
|
+
| `effect/noEffectBind` | Bans Effect.bind |
|
|
52
|
+
| `effect/preferCatchTag` | Replaces manual `_tag` predicates and `catchAll` dispatch with tagged recovery |
|
|
53
|
+
| `effect/preferEffectFn` | Requires `Effect.fn` for a generator operation that adds a span |
|
|
54
|
+
| `effect/preferMatchTagsExhaustive` | Requires exhaustive `Match` for return-only `_tag` switches and if chains |
|
|
55
|
+
| `effect/preferPredicateIsTagged` | Replaces combined `_tag` comparisons with a named `Predicate` refinement |
|
|
56
|
+
| `effect/preferServiceOf` | Checks inline Layer implementations through `Service.of` |
|
|
57
|
+
| `effect/requireNamedEffectFn` | Requires stable names for `Effect.fn` operations |
|
|
58
|
+
| `effect/noGlobals` | Bans ambient capabilities with direct Effect replacements; allows `process.std*.isTTY` |
|
|
59
|
+
| `effect/noNodeBuiltinImport` | Bans fully replaced Node modules and replaced operations from partial modules |
|
|
49
60
|
|
|
50
61
|
## Anti-Slop Rules
|
|
51
62
|
|
|
@@ -69,6 +80,31 @@ See `THIRD_PARTY_NOTICES.md` for the source revision and license.
|
|
|
69
80
|
|
|
70
81
|
The preset intentionally allows `Effect.as`, `Option.as`, `Effect.never`, `Effect.async`, simple `_tag` guards, partial or stateful `switch` statements, and runtime runners at explicit application boundaries.
|
|
71
82
|
|
|
83
|
+
## Complexity Rules
|
|
84
|
+
|
|
85
|
+
The recommended preset caps three complexity metrics for every function. Each nested function is measured as its own unit, so a heavy `Effect.gen` body is reported on the generator, not on the surrounding pipeline.
|
|
86
|
+
|
|
87
|
+
| Rule | Metric | Limit |
|
|
88
|
+
| ------------------------------- | ------------------------------------------ | ----- |
|
|
89
|
+
| `complexity` (native oxlint) | Cyclomatic complexity | 21 |
|
|
90
|
+
| `effect/maxCognitiveComplexity` | Cognitive complexity (SonarSource) | 21 |
|
|
91
|
+
| `effect/maxHalsteadDifficulty` | Halstead difficulty `(η1 / 2) × (N2 / η2)` | 79 |
|
|
92
|
+
|
|
93
|
+
Cognitive complexity charges one for every `if`, loop, `switch`, `catch`, and ternary, plus one for each level of nesting the structure sits in. `else` and `else if` charge one without nesting. Each run of like logical operators (`&&`, `||`, `??`) charges one, and so does a labelled `break` or `continue`.
|
|
94
|
+
|
|
95
|
+
Halstead difficulty treats every name and literal as an operand and every piece of syntax that acts on them as an operator: calls, member access, keywords, declarations, and arithmetic, logical, and assignment operators. Type annotations and type-only declarations never count.
|
|
96
|
+
|
|
97
|
+
Tighten or relax a limit per project or per file by passing a different `max`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"rules": {
|
|
102
|
+
"effect/maxCognitiveComplexity": ["error", { "max": 15 }],
|
|
103
|
+
"effect/maxHalsteadDifficulty": ["error", { "max": 60 }]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
72
108
|
## Platform Boundaries
|
|
73
109
|
|
|
74
110
|
Application code should use Effect services for time, randomness, crypto randomness and supported digests, configuration, files, paths, child processes, stdio, HTTP, sockets, workers, streams, logging, JSON boundaries, encoding, and key-value storage.
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* AUTO-GENERATED by scripts/codegen.ts — do not edit manually.
|
|
6
6
|
*/
|
|
7
7
|
declare const recommended: {
|
|
8
|
+
readonly complexity: readonly ["error", {
|
|
9
|
+
readonly max: 21;
|
|
10
|
+
}];
|
|
11
|
+
readonly "effect/maxCognitiveComplexity": readonly ["error", {
|
|
12
|
+
readonly max: 21;
|
|
13
|
+
}];
|
|
14
|
+
readonly "effect/maxHalsteadDifficulty": readonly ["error", {
|
|
15
|
+
readonly max: 79;
|
|
16
|
+
}];
|
|
8
17
|
readonly "effect/noAs": "error";
|
|
9
18
|
readonly "effect/noAsyncFunction": "error";
|
|
10
19
|
readonly "effect/noChainedTypeAssertions": "error";
|
|
@@ -13,19 +22,28 @@ declare const recommended: {
|
|
|
13
22
|
readonly "effect/noEffectBind": "error";
|
|
14
23
|
readonly "effect/noEffectDo": "error";
|
|
15
24
|
readonly "effect/noGlobals": "error";
|
|
25
|
+
readonly "effect/noInlineProvide": "error";
|
|
16
26
|
readonly "effect/noKnownValueWidening": "error";
|
|
27
|
+
readonly "effect/noManagedRuntimeInEffect": "error";
|
|
17
28
|
readonly "effect/noModuleMocks": "error";
|
|
29
|
+
readonly "effect/noNestedEffectGen": "error";
|
|
18
30
|
readonly "effect/noNewError": "error";
|
|
19
31
|
readonly "effect/noNewPromise": "error";
|
|
20
32
|
readonly "effect/noNodeBuiltinImport": "error";
|
|
21
33
|
readonly "effect/noNullish": "error";
|
|
22
34
|
readonly "effect/noObjectParameters": "error";
|
|
35
|
+
readonly "effect/noPerCallCacheConstruction": "error";
|
|
36
|
+
readonly "effect/noRunCollectOnUnboundedStream": "error";
|
|
23
37
|
readonly "effect/noRuntimeTypeof": "error";
|
|
38
|
+
readonly "effect/noSequentialEffectAll": "error";
|
|
24
39
|
readonly "effect/noShapeInSymbolNames": "error";
|
|
40
|
+
readonly "effect/noSilentCatchAll": "error";
|
|
25
41
|
readonly "effect/noTernary": "error";
|
|
26
42
|
readonly "effect/noTestLifecycleHooks": "error";
|
|
27
43
|
readonly "effect/noThrowStatement": "error";
|
|
28
44
|
readonly "effect/noTryCatch": "error";
|
|
45
|
+
readonly "effect/noUnboundedConcurrency": "error";
|
|
46
|
+
readonly "effect/noUnboundedRetry": "error";
|
|
29
47
|
readonly "effect/noUnknownParameters": "error";
|
|
30
48
|
readonly "effect/noUnknownTypeAliases": "error";
|
|
31
49
|
readonly "effect/noUnsafeDictionaryType": "error";
|
|
@@ -34,6 +52,8 @@ declare const recommended: {
|
|
|
34
52
|
readonly "effect/preferEffectFn": "error";
|
|
35
53
|
readonly "effect/preferMatchTagsExhaustive": "error";
|
|
36
54
|
readonly "effect/preferPredicateIsTagged": "error";
|
|
55
|
+
readonly "effect/preferServiceOf": "error";
|
|
56
|
+
readonly "effect/requireNamedEffectFn": "error";
|
|
37
57
|
};
|
|
38
58
|
//#endregion
|
|
39
59
|
export { recommended };
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* AUTO-GENERATED by scripts/codegen.ts — do not edit manually.
|
|
6
6
|
*/
|
|
7
7
|
const recommended = {
|
|
8
|
+
complexity: ["error", { max: 21 }],
|
|
9
|
+
"effect/maxCognitiveComplexity": ["error", { max: 21 }],
|
|
10
|
+
"effect/maxHalsteadDifficulty": ["error", { max: 79 }],
|
|
8
11
|
"effect/noAs": "error",
|
|
9
12
|
"effect/noAsyncFunction": "error",
|
|
10
13
|
"effect/noChainedTypeAssertions": "error",
|
|
@@ -13,19 +16,28 @@ const recommended = {
|
|
|
13
16
|
"effect/noEffectBind": "error",
|
|
14
17
|
"effect/noEffectDo": "error",
|
|
15
18
|
"effect/noGlobals": "error",
|
|
19
|
+
"effect/noInlineProvide": "error",
|
|
16
20
|
"effect/noKnownValueWidening": "error",
|
|
21
|
+
"effect/noManagedRuntimeInEffect": "error",
|
|
17
22
|
"effect/noModuleMocks": "error",
|
|
23
|
+
"effect/noNestedEffectGen": "error",
|
|
18
24
|
"effect/noNewError": "error",
|
|
19
25
|
"effect/noNewPromise": "error",
|
|
20
26
|
"effect/noNodeBuiltinImport": "error",
|
|
21
27
|
"effect/noNullish": "error",
|
|
22
28
|
"effect/noObjectParameters": "error",
|
|
29
|
+
"effect/noPerCallCacheConstruction": "error",
|
|
30
|
+
"effect/noRunCollectOnUnboundedStream": "error",
|
|
23
31
|
"effect/noRuntimeTypeof": "error",
|
|
32
|
+
"effect/noSequentialEffectAll": "error",
|
|
24
33
|
"effect/noShapeInSymbolNames": "error",
|
|
34
|
+
"effect/noSilentCatchAll": "error",
|
|
25
35
|
"effect/noTernary": "error",
|
|
26
36
|
"effect/noTestLifecycleHooks": "error",
|
|
27
37
|
"effect/noThrowStatement": "error",
|
|
28
38
|
"effect/noTryCatch": "error",
|
|
39
|
+
"effect/noUnboundedConcurrency": "error",
|
|
40
|
+
"effect/noUnboundedRetry": "error",
|
|
29
41
|
"effect/noUnknownParameters": "error",
|
|
30
42
|
"effect/noUnknownTypeAliases": "error",
|
|
31
43
|
"effect/noUnsafeDictionaryType": "error",
|
|
@@ -33,7 +45,9 @@ const recommended = {
|
|
|
33
45
|
"effect/preferCatchTag": "error",
|
|
34
46
|
"effect/preferEffectFn": "error",
|
|
35
47
|
"effect/preferMatchTagsExhaustive": "error",
|
|
36
|
-
"effect/preferPredicateIsTagged": "error"
|
|
48
|
+
"effect/preferPredicateIsTagged": "error",
|
|
49
|
+
"effect/preferServiceOf": "error",
|
|
50
|
+
"effect/requireNamedEffectFn": "error"
|
|
37
51
|
};
|
|
38
52
|
//#endregion
|
|
39
53
|
export { recommended };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isStaticCall, isStaticMember } from "./_effect-namespaces.js";
|
|
2
|
+
//#region src/rules/_effect-context.ts
|
|
3
|
+
const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
4
|
+
const effectProgramKind = (node, effectNamespaces) => {
|
|
5
|
+
const parent = node.parent;
|
|
6
|
+
if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return void 0;
|
|
7
|
+
if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "gen")) return "gen";
|
|
8
|
+
if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "fn")) return "fn";
|
|
9
|
+
if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "fnUntraced")) return "fnUntraced";
|
|
10
|
+
if (parent.callee.type !== "CallExpression") return void 0;
|
|
11
|
+
if (isStaticCall(parent.callee, effectNamespaces, "fn")) return "fn";
|
|
12
|
+
if (isStaticCall(parent.callee, effectNamespaces, "fnUntraced")) return "fnUntraced";
|
|
13
|
+
};
|
|
14
|
+
const enclosingEffectProgram = (node, effectNamespaces) => {
|
|
15
|
+
let current = node.parent;
|
|
16
|
+
while (current !== void 0 && current !== null) {
|
|
17
|
+
if (isFunction(current)) {
|
|
18
|
+
const kind = effectProgramKind(current, effectNamespaces);
|
|
19
|
+
if (kind === void 0) return void 0;
|
|
20
|
+
return [current, kind];
|
|
21
|
+
}
|
|
22
|
+
current = current.parent;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const isInsideEffectProgram = (node, effectNamespaces) => enclosingEffectProgram(node, effectNamespaces) !== void 0;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { enclosingEffectProgram, isInsideEffectProgram };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
|
|
2
|
+
import "../vendor/effect-oxlint/index.js";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
//#region src/rules/_effect-namespaces.ts
|
|
5
|
+
const importedNamespaces = (node, exportName, directModule) => {
|
|
6
|
+
const source = node.source.value;
|
|
7
|
+
if (source !== "effect" && source !== directModule) return [];
|
|
8
|
+
const namespaces = [];
|
|
9
|
+
for (const specifier of node.specifiers) {
|
|
10
|
+
if (specifier.type === "ImportNamespaceSpecifier" && source === directModule) {
|
|
11
|
+
namespaces.push(specifier.local.name);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (specifier.type === "ImportSpecifier" && source === "effect" && specifier.imported.type === "Identifier" && specifier.imported.name === exportName) namespaces.push(specifier.local.name);
|
|
15
|
+
}
|
|
16
|
+
return namespaces;
|
|
17
|
+
};
|
|
18
|
+
const visibleNamespaces = (ctx, node, namespaces) => new Set([...namespaces].filter((namespace) => Option.match(findVariableUp(ctx.sourceCode.getScope(node), namespace), {
|
|
19
|
+
onNone: () => true,
|
|
20
|
+
onSome: (variable) => variable.defs.some((definition) => definition.type === "ImportBinding")
|
|
21
|
+
})));
|
|
22
|
+
const isStaticMember = (node, namespaces, property) => node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && namespaces.has(node.object.name) && node.property.type === "Identifier" && node.property.name === property;
|
|
23
|
+
const isStaticCall = (node, namespaces, property) => node.type === "CallExpression" && node.callee.type !== "Super" && isStaticMember(node.callee, namespaces, property);
|
|
24
|
+
//#endregion
|
|
25
|
+
export { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
//#region src/rules/_function-metrics.ts
|
|
2
|
+
const isNode = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
3
|
+
const isFunctionType = (type) => type === "ArrowFunctionExpression" || type === "FunctionDeclaration" || type === "FunctionExpression";
|
|
4
|
+
const isFunctionNode = (node) => isFunctionType(node.type);
|
|
5
|
+
/** Slots that hold the parent link, source positions, or type-level syntax. */
|
|
6
|
+
const structuralKeys = new Set([
|
|
7
|
+
"parent",
|
|
8
|
+
"loc",
|
|
9
|
+
"range",
|
|
10
|
+
"start",
|
|
11
|
+
"end",
|
|
12
|
+
"typeAnnotation",
|
|
13
|
+
"returnType",
|
|
14
|
+
"typeParameters",
|
|
15
|
+
"typeArguments",
|
|
16
|
+
"superTypeArguments",
|
|
17
|
+
"implements",
|
|
18
|
+
"decorators"
|
|
19
|
+
]);
|
|
20
|
+
/** TypeScript expression wrappers whose `expression` slot is runtime code. */
|
|
21
|
+
const transparentTypeWrappers = new Set([
|
|
22
|
+
"TSAsExpression",
|
|
23
|
+
"TSSatisfiesExpression",
|
|
24
|
+
"TSNonNullExpression",
|
|
25
|
+
"TSTypeAssertion",
|
|
26
|
+
"TSInstantiationExpression"
|
|
27
|
+
]);
|
|
28
|
+
const isTypeOnly = (node) => node.type.startsWith("TS") && !transparentTypeWrappers.has(node.type);
|
|
29
|
+
const slot = (node, key) => {
|
|
30
|
+
const value = node[key];
|
|
31
|
+
if (isNode(value)) return [value];
|
|
32
|
+
if (Array.isArray(value)) return value.filter(isNode);
|
|
33
|
+
return [];
|
|
34
|
+
};
|
|
35
|
+
/** Runtime child nodes of `node`, in source order of their slots. */
|
|
36
|
+
const children = (node) => {
|
|
37
|
+
if (transparentTypeWrappers.has(node.type)) return slot(node, "expression");
|
|
38
|
+
const found = [];
|
|
39
|
+
for (const key of Object.keys(node)) {
|
|
40
|
+
if (structuralKeys.has(key)) continue;
|
|
41
|
+
for (const child of slot(node, key)) if (!isTypeOnly(child)) found.push(child);
|
|
42
|
+
}
|
|
43
|
+
return found;
|
|
44
|
+
};
|
|
45
|
+
/** The nodes that form the body of a function unit: its parameters and its body. */
|
|
46
|
+
const unitRoots = (fn) => {
|
|
47
|
+
const node = fn;
|
|
48
|
+
return [...slot(node, "params"), ...slot(node, "body")];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Flatten a logical expression tree into its operator sequence, left to right.
|
|
52
|
+
* `a && b || c` yields `["&&", "||"]`.
|
|
53
|
+
*/
|
|
54
|
+
const logicalOperators = (node, into) => {
|
|
55
|
+
if (node.type !== "LogicalExpression") return;
|
|
56
|
+
for (const side of slot(node, "left")) logicalOperators(side, into);
|
|
57
|
+
into.push(String(node.operator));
|
|
58
|
+
for (const side of slot(node, "right")) logicalOperators(side, into);
|
|
59
|
+
};
|
|
60
|
+
/** Leaf operands of a logical expression tree, in source order. */
|
|
61
|
+
const logicalLeaves = (node, into) => {
|
|
62
|
+
if (node.type !== "LogicalExpression") {
|
|
63
|
+
into.push(node);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const side of slot(node, "left")) logicalLeaves(side, into);
|
|
67
|
+
for (const side of slot(node, "right")) logicalLeaves(side, into);
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Cognitive complexity of one function, excluding nested functions.
|
|
71
|
+
*
|
|
72
|
+
* - `if`, loops, `switch`, `catch`, and `?:` cost one plus the current nesting level.
|
|
73
|
+
* - `else` and `else if` cost one and never pay for nesting.
|
|
74
|
+
* - A labelled `break` or `continue` costs one.
|
|
75
|
+
* - Each run of like logical operators (`&&`, `||`, `??`) costs one.
|
|
76
|
+
* - The bodies of nesting structures raise the nesting level by one.
|
|
77
|
+
*/
|
|
78
|
+
const cognitiveComplexity = (fn) => {
|
|
79
|
+
let total = 0;
|
|
80
|
+
const visitSlot = (node, key, nesting) => {
|
|
81
|
+
for (const child of slot(node, key)) visit(child, nesting);
|
|
82
|
+
};
|
|
83
|
+
const visitIf = (node, nesting, isElseIf) => {
|
|
84
|
+
total += isElseIf ? 1 : 1 + nesting;
|
|
85
|
+
visitSlot(node, "test", nesting);
|
|
86
|
+
visitSlot(node, "consequent", nesting + 1);
|
|
87
|
+
for (const alternate of slot(node, "alternate")) if (alternate.type === "IfStatement") visitIf(alternate, nesting, true);
|
|
88
|
+
else {
|
|
89
|
+
total += 1;
|
|
90
|
+
visit(alternate, nesting + 1);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const visitNesting = (node, nesting, headerKeys, bodyKeys) => {
|
|
94
|
+
total += 1 + nesting;
|
|
95
|
+
for (const key of headerKeys) visitSlot(node, key, nesting);
|
|
96
|
+
for (const key of bodyKeys) visitSlot(node, key, nesting + 1);
|
|
97
|
+
};
|
|
98
|
+
const visit = (node, nesting) => {
|
|
99
|
+
if (isTypeOnly(node)) return;
|
|
100
|
+
switch (node.type) {
|
|
101
|
+
case "ArrowFunctionExpression":
|
|
102
|
+
case "FunctionDeclaration":
|
|
103
|
+
case "FunctionExpression": return;
|
|
104
|
+
case "IfStatement": return visitIf(node, nesting, false);
|
|
105
|
+
case "ConditionalExpression": return visitNesting(node, nesting, ["test"], ["consequent", "alternate"]);
|
|
106
|
+
case "SwitchStatement": return visitNesting(node, nesting, ["discriminant"], ["cases"]);
|
|
107
|
+
case "ForStatement": return visitNesting(node, nesting, [
|
|
108
|
+
"init",
|
|
109
|
+
"test",
|
|
110
|
+
"update"
|
|
111
|
+
], ["body"]);
|
|
112
|
+
case "ForInStatement":
|
|
113
|
+
case "ForOfStatement": return visitNesting(node, nesting, ["left", "right"], ["body"]);
|
|
114
|
+
case "WhileStatement":
|
|
115
|
+
case "DoWhileStatement": return visitNesting(node, nesting, ["test"], ["body"]);
|
|
116
|
+
case "CatchClause": return visitNesting(node, nesting, ["param"], ["body"]);
|
|
117
|
+
case "BreakStatement":
|
|
118
|
+
case "ContinueStatement":
|
|
119
|
+
if (isNode(node.label)) total += 1;
|
|
120
|
+
return;
|
|
121
|
+
case "LogicalExpression": {
|
|
122
|
+
const operators = [];
|
|
123
|
+
logicalOperators(node, operators);
|
|
124
|
+
total += operators.filter((operator, index) => operator !== operators[index - 1]).length;
|
|
125
|
+
const leaves = [];
|
|
126
|
+
logicalLeaves(node, leaves);
|
|
127
|
+
for (const leaf of leaves) visit(leaf, nesting);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
default: for (const child of children(node)) visit(child, nesting);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
for (const root of unitRoots(fn)) visit(root, 0);
|
|
134
|
+
return total;
|
|
135
|
+
};
|
|
136
|
+
/** Halstead difficulty: `(η1 / 2) × (N2 / η2)`. Zero when the unit has no operands. */
|
|
137
|
+
const halsteadDifficulty = (counts) => {
|
|
138
|
+
if (counts.distinctOperands === 0) return 0;
|
|
139
|
+
return counts.distinctOperators / 2 * (counts.totalOperands / counts.distinctOperands);
|
|
140
|
+
};
|
|
141
|
+
const functionOperator = (node) => {
|
|
142
|
+
if (node.type === "ArrowFunctionExpression") return "=>";
|
|
143
|
+
return node.generator === true ? "function*" : "function";
|
|
144
|
+
};
|
|
145
|
+
const literalOperand = (node) => {
|
|
146
|
+
if (typeof node.raw === "string") return node.raw;
|
|
147
|
+
if (typeof node.bigint === "string") return `${node.bigint}n`;
|
|
148
|
+
return String(node.value);
|
|
149
|
+
};
|
|
150
|
+
const templateOperand = (node) => {
|
|
151
|
+
const value = node.value;
|
|
152
|
+
if (typeof value !== "object" || value === null) return null;
|
|
153
|
+
const raw = value.raw;
|
|
154
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
155
|
+
};
|
|
156
|
+
/** Node types that always stand for the same operator tokens. */
|
|
157
|
+
const fixedOperators = {
|
|
158
|
+
ForStatement: ["for"],
|
|
159
|
+
ForInStatement: ["for-in"],
|
|
160
|
+
WhileStatement: ["while"],
|
|
161
|
+
DoWhileStatement: ["do-while"],
|
|
162
|
+
SwitchStatement: ["switch"],
|
|
163
|
+
ReturnStatement: ["return"],
|
|
164
|
+
ThrowStatement: ["throw"],
|
|
165
|
+
CatchClause: ["catch"],
|
|
166
|
+
BreakStatement: ["break"],
|
|
167
|
+
ContinueStatement: ["continue"],
|
|
168
|
+
LabeledStatement: [":"],
|
|
169
|
+
DebuggerStatement: ["debugger"],
|
|
170
|
+
WithStatement: ["with"],
|
|
171
|
+
AssignmentPattern: ["="],
|
|
172
|
+
NewExpression: ["new"],
|
|
173
|
+
ArrayExpression: ["[]"],
|
|
174
|
+
ArrayPattern: ["[]"],
|
|
175
|
+
ObjectExpression: ["{}"],
|
|
176
|
+
ObjectPattern: ["{}"],
|
|
177
|
+
SpreadElement: ["..."],
|
|
178
|
+
RestElement: ["..."],
|
|
179
|
+
ConditionalExpression: ["?:"],
|
|
180
|
+
SequenceExpression: [","],
|
|
181
|
+
AwaitExpression: ["await"],
|
|
182
|
+
ImportExpression: ["import()"],
|
|
183
|
+
TemplateLiteral: ["`"],
|
|
184
|
+
JSXElement: ["<>"],
|
|
185
|
+
JSXFragment: ["<>"]
|
|
186
|
+
};
|
|
187
|
+
/** Node types whose operator token carries the operator itself. */
|
|
188
|
+
const operatorCarriers = new Set([
|
|
189
|
+
"BinaryExpression",
|
|
190
|
+
"LogicalExpression",
|
|
191
|
+
"AssignmentExpression",
|
|
192
|
+
"UnaryExpression",
|
|
193
|
+
"UpdateExpression"
|
|
194
|
+
]);
|
|
195
|
+
/** Statement-level nodes whose operator tokens depend on the node's shape. */
|
|
196
|
+
const statementOperators = (node) => {
|
|
197
|
+
switch (node.type) {
|
|
198
|
+
case "IfStatement": return isNode(node.alternate) ? ["if", "else"] : ["if"];
|
|
199
|
+
case "ForOfStatement": return node.await === true ? ["for-await-of"] : ["for-of"];
|
|
200
|
+
case "SwitchCase": return isNode(node.test) ? ["case"] : ["default"];
|
|
201
|
+
case "TryStatement": return isNode(node.finalizer) ? ["try", "finally"] : ["try"];
|
|
202
|
+
case "VariableDeclaration": return [String(node.kind)];
|
|
203
|
+
case "VariableDeclarator": return isNode(node.init) ? ["="] : [];
|
|
204
|
+
case "ClassDeclaration":
|
|
205
|
+
case "ClassExpression": return isNode(node.superClass) ? ["class", "extends"] : ["class"];
|
|
206
|
+
case "PropertyDefinition": return isNode(node.value) ? ["="] : [];
|
|
207
|
+
default: return [];
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/** Expression-level nodes whose operator tokens depend on the node's shape. */
|
|
211
|
+
const expressionOperators = (node) => {
|
|
212
|
+
if (operatorCarriers.has(node.type)) return [String(node.operator)];
|
|
213
|
+
switch (node.type) {
|
|
214
|
+
case "CallExpression": return node.optional === true ? ["?.()"] : ["()"];
|
|
215
|
+
case "MemberExpression": {
|
|
216
|
+
const access = node.computed === true ? "[]" : ".";
|
|
217
|
+
return node.optional === true ? [`?${access}`] : [access];
|
|
218
|
+
}
|
|
219
|
+
case "Property":
|
|
220
|
+
if (node.kind === "get" || node.kind === "set") return [String(node.kind)];
|
|
221
|
+
return node.shorthand === true || node.method === true ? [] : [":"];
|
|
222
|
+
case "YieldExpression": return node.delegate === true ? ["yield*"] : ["yield"];
|
|
223
|
+
case "JSXAttribute": return isNode(node.value) ? ["="] : [];
|
|
224
|
+
case "ArrowFunctionExpression":
|
|
225
|
+
case "FunctionDeclaration":
|
|
226
|
+
case "FunctionExpression": return node.async === true ? ["async", functionOperator(node)] : [functionOperator(node)];
|
|
227
|
+
default: return [];
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
/** Every operator token a node stands for. */
|
|
231
|
+
const operatorsOf = (node) => [
|
|
232
|
+
...fixedOperators[node.type] ?? [],
|
|
233
|
+
...statementOperators(node),
|
|
234
|
+
...expressionOperators(node)
|
|
235
|
+
];
|
|
236
|
+
/** The operand a leaf node contributes, if any. */
|
|
237
|
+
const operandOf = (node) => {
|
|
238
|
+
switch (node.type) {
|
|
239
|
+
case "Identifier":
|
|
240
|
+
case "JSXIdentifier": return String(node.name);
|
|
241
|
+
case "PrivateIdentifier": return `#${String(node.name)}`;
|
|
242
|
+
case "Literal": return literalOperand(node);
|
|
243
|
+
case "TemplateElement": return templateOperand(node);
|
|
244
|
+
case "ThisExpression": return "this";
|
|
245
|
+
case "Super": return "super";
|
|
246
|
+
case "MetaProperty": return `${isNode(node.meta) ? String(node.meta.name) : ""}.${isNode(node.property) ? String(node.property.name) : ""}`;
|
|
247
|
+
case "JSXText": {
|
|
248
|
+
const text = String(node.value).trim();
|
|
249
|
+
return text.length > 0 ? text : null;
|
|
250
|
+
}
|
|
251
|
+
default: return null;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Halstead operator and operand counts of one function, excluding nested
|
|
256
|
+
* functions. A nested function contributes only its own `function` or `=>`
|
|
257
|
+
* operator to the enclosing unit.
|
|
258
|
+
*/
|
|
259
|
+
const halstead = (fn) => {
|
|
260
|
+
const operators = /* @__PURE__ */ new Map();
|
|
261
|
+
const operands = /* @__PURE__ */ new Map();
|
|
262
|
+
const tally = (into, token) => {
|
|
263
|
+
into.set(token, (into.get(token) ?? 0) + 1);
|
|
264
|
+
};
|
|
265
|
+
const visit = (node) => {
|
|
266
|
+
if (isTypeOnly(node)) return;
|
|
267
|
+
for (const operator of operatorsOf(node)) tally(operators, operator);
|
|
268
|
+
const operand = operandOf(node);
|
|
269
|
+
if (operand !== null) tally(operands, operand);
|
|
270
|
+
if (isFunctionType(node.type) || node.type === "MetaProperty") return;
|
|
271
|
+
for (const child of children(node)) visit(child);
|
|
272
|
+
};
|
|
273
|
+
for (const root of unitRoots(fn)) visit(root);
|
|
274
|
+
const total = (counts) => Array.from(counts.values()).reduce((sum, count) => sum + count, 0);
|
|
275
|
+
return {
|
|
276
|
+
distinctOperators: operators.size,
|
|
277
|
+
totalOperators: total(operators),
|
|
278
|
+
distinctOperands: operands.size,
|
|
279
|
+
totalOperands: total(operands)
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
//#endregion
|
|
283
|
+
export { cognitiveComplexity, halstead, halsteadDifficulty, isFunctionNode };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
//#region src/rules/_max-option.ts
|
|
3
|
+
const MaxOptions = Schema.UndefinedOr(Schema.Struct({ max: Schema.optionalKey(Schema.Number) }));
|
|
4
|
+
const maxOptionsMeta = (defaultMax) => ({
|
|
5
|
+
schema: [{
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: { max: {
|
|
8
|
+
type: "integer",
|
|
9
|
+
minimum: 0
|
|
10
|
+
} },
|
|
11
|
+
additionalProperties: false
|
|
12
|
+
}],
|
|
13
|
+
defaultOptions: [{ max: defaultMax }],
|
|
14
|
+
recommendedOptions: { max: defaultMax }
|
|
15
|
+
});
|
|
16
|
+
/** The configured limit, or the rule default when the harness supplies no options. */
|
|
17
|
+
const resolveMax = (options, defaultMax) => options?.max ?? defaultMax;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { MaxOptions, maxOptionsMeta, resolveMax };
|
|
@@ -76,4 +76,4 @@ const isInsideCatchAllHandler = (node) => {
|
|
|
76
76
|
};
|
|
77
77
|
const taggedSwitchSubject = (node) => taggedSubject(node.discriminant);
|
|
78
78
|
//#endregion
|
|
79
|
-
export { effectCallbackLocation, hasOneTaggedSubject, isEffectCall, isInsideCatchAllHandler, tagComparisonsInOr, taggedSwitchSubject };
|
|
79
|
+
export { effectCallbackLocation, hasOneTaggedSubject, isEffectCall, isInsideCatchAllHandler, tagComparison, tagComparisonsInOr, taggedSwitchSubject };
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
import { noEffectBind } from "./no-effect-bind.js";
|
|
2
2
|
import { noEffectDo } from "./no-effect-do.js";
|
|
3
|
+
import { maxCognitiveComplexity } from "./max-cognitive-complexity.js";
|
|
4
|
+
import { maxHalsteadDifficulty } from "./max-halstead-difficulty.js";
|
|
3
5
|
import { noAs } from "./no-as.js";
|
|
4
6
|
import { noAsyncFunction } from "./no-async-function.js";
|
|
5
7
|
import { noChainedTypeAssertions } from "./no-chained-type-assertions.js";
|
|
6
8
|
import { noConditionalEmptyObjectSpread } from "./no-conditional-empty-object-spread.js";
|
|
7
9
|
import { noDynamicImports } from "./no-dynamic-imports.js";
|
|
8
10
|
import { noGlobals } from "./no-globals.js";
|
|
11
|
+
import { noInlineProvide } from "./no-inline-provide.js";
|
|
9
12
|
import { noKnownValueWidening } from "./no-known-value-widening.js";
|
|
13
|
+
import { noManagedRuntimeInEffect } from "./no-managed-runtime-in-effect.js";
|
|
10
14
|
import { noModuleMocks } from "./no-module-mocks.js";
|
|
15
|
+
import { noNestedEffectGen } from "./no-nested-effect-gen.js";
|
|
11
16
|
import { noNewError } from "./no-new-error.js";
|
|
12
17
|
import { noNewPromise } from "./no-new-promise.js";
|
|
13
18
|
import { noNodeBuiltinImport } from "./no-node-builtin-import.js";
|
|
14
19
|
import { noNullish } from "./no-nullish.js";
|
|
15
20
|
import { noObjectParameters } from "./no-object-parameters.js";
|
|
21
|
+
import { noPerCallCacheConstruction } from "./no-per-call-cache-construction.js";
|
|
22
|
+
import { noRunCollectOnUnboundedStream } from "./no-run-collect-on-unbounded-stream.js";
|
|
16
23
|
import { noRuntimeTypeof } from "./no-runtime-typeof.js";
|
|
24
|
+
import { noSequentialEffectAll } from "./no-sequential-effect-all.js";
|
|
17
25
|
import { noShapeInSymbolNames } from "./no-shape-in-symbol-names.js";
|
|
26
|
+
import { noSilentCatchAll } from "./no-silent-catch-all.js";
|
|
18
27
|
import { noTernary } from "./no-ternary.js";
|
|
19
28
|
import { noTestLifecycleHooks } from "./no-test-lifecycle-hooks.js";
|
|
20
29
|
import { noThrowStatement } from "./no-throw-statement.js";
|
|
21
30
|
import { noTryCatch } from "./no-try-catch.js";
|
|
31
|
+
import { noUnboundedConcurrency } from "./no-unbounded-concurrency.js";
|
|
32
|
+
import { noUnboundedRetry } from "./no-unbounded-retry.js";
|
|
22
33
|
import { noUnknownParameters } from "./no-unknown-parameters.js";
|
|
23
34
|
import { noUnknownTypeAliases } from "./no-unknown-type-aliases.js";
|
|
24
35
|
import { noUnsafeDictionaryType } from "./no-unsafe-dictionary-type.js";
|
|
@@ -27,10 +38,12 @@ import { preferCatchTag } from "./prefer-catch-tag.js";
|
|
|
27
38
|
import { preferEffectFn } from "./prefer-effect-fn.js";
|
|
28
39
|
import { preferMatchTagsExhaustive } from "./prefer-match-tags-exhaustive.js";
|
|
29
40
|
import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
|
|
41
|
+
import { preferServiceOf } from "./prefer-service-of.js";
|
|
42
|
+
import { requireNamedEffectFn } from "./require-named-effect-fn.js";
|
|
30
43
|
|
|
31
44
|
//#region src/rules/index.d.ts
|
|
32
45
|
declare namespace index_d_exports {
|
|
33
|
-
export { noAs, noAsyncFunction, noChainedTypeAssertions, noConditionalEmptyObjectSpread, noDynamicImports, noEffectBind, noEffectDo, noGlobals, noKnownValueWidening, noModuleMocks, noNewError, noNewPromise, noNodeBuiltinImport, noNullish, noObjectParameters, noRuntimeTypeof, noShapeInSymbolNames, noTernary, noTestLifecycleHooks, noThrowStatement, noTryCatch, noUnknownParameters, noUnknownTypeAliases, noUnsafeDictionaryType, noWidenThenAssert, preferCatchTag, preferEffectFn, preferMatchTagsExhaustive, preferPredicateIsTagged };
|
|
46
|
+
export { maxCognitiveComplexity, maxHalsteadDifficulty, noAs, noAsyncFunction, noChainedTypeAssertions, noConditionalEmptyObjectSpread, noDynamicImports, noEffectBind, noEffectDo, noGlobals, noInlineProvide, noKnownValueWidening, noManagedRuntimeInEffect, noModuleMocks, noNestedEffectGen, noNewError, noNewPromise, noNodeBuiltinImport, noNullish, noObjectParameters, noPerCallCacheConstruction, noRunCollectOnUnboundedStream, noRuntimeTypeof, noSequentialEffectAll, noShapeInSymbolNames, noSilentCatchAll, noTernary, noTestLifecycleHooks, noThrowStatement, noTryCatch, noUnboundedConcurrency, noUnboundedRetry, noUnknownParameters, noUnknownTypeAliases, noUnsafeDictionaryType, noWidenThenAssert, preferCatchTag, preferEffectFn, preferMatchTagsExhaustive, preferPredicateIsTagged, preferServiceOf, requireNamedEffectFn };
|
|
34
47
|
}
|
|
35
48
|
//#endregion
|
|
36
49
|
export { index_d_exports };
|