oxlint-plugin-effect 0.11.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 CHANGED
@@ -21,7 +21,7 @@ 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
 
@@ -80,6 +80,31 @@ See `THIRD_PARTY_NOTICES.md` for the source revision and license.
80
80
 
81
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.
82
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
+
83
108
  ## Platform Boundaries
84
109
 
85
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";
@@ -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",
@@ -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 };
@@ -1,5 +1,7 @@
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";
@@ -41,7 +43,7 @@ import { requireNamedEffectFn } from "./require-named-effect-fn.js";
41
43
 
42
44
  //#region src/rules/index.d.ts
43
45
  declare namespace index_d_exports {
44
- export { 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 };
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 };
45
47
  }
46
48
  //#endregion
47
49
  export { index_d_exports };
@@ -1,6 +1,8 @@
1
1
  import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
2
  import { noEffectBind } from "./no-effect-bind.js";
3
3
  import { noEffectDo } from "./no-effect-do.js";
4
+ import { maxCognitiveComplexity } from "./max-cognitive-complexity.js";
5
+ import { maxHalsteadDifficulty } from "./max-halstead-difficulty.js";
4
6
  import { noAs } from "./no-as.js";
5
7
  import { noAsyncFunction } from "./no-async-function.js";
6
8
  import { noChainedTypeAssertions } from "./no-chained-type-assertions.js";
@@ -41,6 +43,8 @@ import { preferServiceOf } from "./prefer-service-of.js";
41
43
  import { requireNamedEffectFn } from "./require-named-effect-fn.js";
42
44
  //#region src/rules/index.ts
43
45
  var rules_exports = /* @__PURE__ */ __exportAll({
46
+ maxCognitiveComplexity: () => maxCognitiveComplexity,
47
+ maxHalsteadDifficulty: () => maxHalsteadDifficulty,
44
48
  noAs: () => noAs,
45
49
  noAsyncFunction: () => noAsyncFunction,
46
50
  noChainedTypeAssertions: () => noChainedTypeAssertions,
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/max-cognitive-complexity.d.ts
4
+ declare const maxCognitiveComplexity: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { maxCognitiveComplexity };
@@ -0,0 +1,43 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { cognitiveComplexity, isFunctionNode } from "./_function-metrics.js";
6
+ import { MaxOptions, maxOptionsMeta, resolveMax } from "./_max-option.js";
7
+ import * as Effect from "effect/Effect";
8
+ //#region src/rules/max-cognitive-complexity.ts
9
+ const DEFAULT_MAX = 21;
10
+ const maxCognitiveComplexity = define({
11
+ name: "max-cognitive-complexity",
12
+ meta: meta({
13
+ type: "suggestion",
14
+ description: "Keep every function below the cognitive complexity limit.",
15
+ messages: { tooComplex: "Function has a cognitive complexity of {{value}}. Maximum allowed is {{max}}. Split it into named operations." },
16
+ ...maxOptionsMeta(DEFAULT_MAX)
17
+ }),
18
+ options: MaxOptions,
19
+ create: function* (options) {
20
+ const context = yield* RuleContext;
21
+ const max = resolveMax(options, DEFAULT_MAX);
22
+ const check = (node) => {
23
+ if (!isFunctionNode(node)) return Effect.void;
24
+ const value = cognitiveComplexity(node);
25
+ if (value <= max) return Effect.void;
26
+ return context.report(fromId({
27
+ node,
28
+ messageId: "tooComplex",
29
+ data: {
30
+ value: String(value),
31
+ max: String(max)
32
+ }
33
+ }));
34
+ };
35
+ return {
36
+ ArrowFunctionExpression: check,
37
+ FunctionDeclaration: check,
38
+ FunctionExpression: check
39
+ };
40
+ }
41
+ });
42
+ //#endregion
43
+ export { maxCognitiveComplexity };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/max-halstead-difficulty.d.ts
4
+ declare const maxHalsteadDifficulty: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { maxHalsteadDifficulty };
@@ -0,0 +1,44 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { halstead, halsteadDifficulty, isFunctionNode } from "./_function-metrics.js";
6
+ import { MaxOptions, maxOptionsMeta, resolveMax } from "./_max-option.js";
7
+ import * as Effect from "effect/Effect";
8
+ //#region src/rules/max-halstead-difficulty.ts
9
+ const DEFAULT_MAX = 79;
10
+ const formatDifficulty = (value) => String(Math.round(value * 10) / 10);
11
+ const maxHalsteadDifficulty = define({
12
+ name: "max-halstead-difficulty",
13
+ meta: meta({
14
+ type: "suggestion",
15
+ description: "Keep every function below the Halstead difficulty limit.",
16
+ messages: { tooDifficult: "Function has a Halstead difficulty of {{value}}. Maximum allowed is {{max}}. Extract repeated expressions into named values." },
17
+ ...maxOptionsMeta(DEFAULT_MAX)
18
+ }),
19
+ options: MaxOptions,
20
+ create: function* (options) {
21
+ const context = yield* RuleContext;
22
+ const max = resolveMax(options, DEFAULT_MAX);
23
+ const check = (node) => {
24
+ if (!isFunctionNode(node)) return Effect.void;
25
+ const value = halsteadDifficulty(halstead(node));
26
+ if (value <= max) return Effect.void;
27
+ return context.report(fromId({
28
+ node,
29
+ messageId: "tooDifficult",
30
+ data: {
31
+ value: formatDifficulty(value),
32
+ max: String(max)
33
+ }
34
+ }));
35
+ };
36
+ return {
37
+ ArrowFunctionExpression: check,
38
+ FunctionDeclaration: check,
39
+ FunctionExpression: check
40
+ };
41
+ }
42
+ });
43
+ //#endregion
44
+ export { maxHalsteadDifficulty };
@@ -71,7 +71,17 @@ declare const meta: (opts: {
71
71
  readonly hasSuggestions?: boolean | undefined;
72
72
  readonly messages?: Record<string, string> | undefined;
73
73
  readonly docs?: RuleDocs | undefined;
74
- readonly effectVersion?: EffectVersion | undefined;
74
+ readonly effectVersion?: EffectVersion | undefined; /** JSON schema for the rule options. Oxlint rejects options on rules without one. */
75
+ readonly schema?: RuleMeta["schema"] | undefined; /** Options oxlint merges underneath the user's configuration. */
76
+ readonly defaultOptions?: RuleMeta["defaultOptions"] | undefined;
77
+ /**
78
+ * Options the recommended preset passes to this rule.
79
+ *
80
+ * Stored on `meta.docs.recommendedOptions`. The preset generator emits
81
+ * `["error", recommendedOptions]` instead of a bare `"error"` entry, so
82
+ * the rule's default threshold and the published preset stay in sync.
83
+ */
84
+ readonly recommendedOptions?: Record<string, unknown> | undefined;
75
85
  }) => RuleMeta;
76
86
  /**
77
87
  * Create a rule that bans `obj.prop` member expression access.
@@ -66,9 +66,12 @@ const meta = (opts) => ({
66
66
  ...opts.fixable !== void 0 ? { fixable: opts.fixable } : {},
67
67
  ...opts.hasSuggestions !== void 0 ? { hasSuggestions: opts.hasSuggestions } : {},
68
68
  ...opts.messages !== void 0 ? { messages: opts.messages } : {},
69
+ ...opts.schema !== void 0 ? { schema: opts.schema } : {},
70
+ ...opts.defaultOptions !== void 0 ? { defaultOptions: opts.defaultOptions } : {},
69
71
  docs: {
70
72
  description: opts.description,
71
73
  effectVersion: opts.effectVersion ?? "both",
74
+ ...opts.recommendedOptions !== void 0 ? { recommendedOptions: opts.recommendedOptions } : {},
72
75
  ...opts.docs
73
76
  }
74
77
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-effect",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "oxlint plugin with Effect-specific lint rules",
5
5
  "license": "MIT",
6
6
  "repository": {