eslint-plugin-functype 2.104.1 → 2.104.3

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.
@@ -1,5 +1,5 @@
1
- import{getFunctypeImports as e,isChainedMethodCall as t}from"../utils/functype-detection.js";const n=new Set([`Option`,`Either`,`Try`,`Task`]),r=new Set([`parent`,`loc`,`range`]),i=(e,t)=>new Set([...e,...t]);function a(e){if(e.type!==`CallExpression`)return new Set;let t=e.callee;return t?.type===`Identifier`&&n.has(t.name)?new Set([t.name]):t?.type===`MemberExpression`&&t.object?.type===`Identifier`&&n.has(t.object.name)?new Set([t.object.name]):new Set}function o(e){return Object.entries(e).filter(([e])=>!r.has(e)).flatMap(([,e])=>Array.isArray(e)?e:[e]).filter(e=>typeof e==`object`&&!!e)}function s(e){if(!e||typeof e!=`object`)return new Set;let t=e;return o(t).reduce((e,t)=>i(e,s(t)),a(t))}const c=new Set([`flatMap`,`map`,`filter`,`fold`]);function l(e){if(e.callee.type!==`MemberExpression`||e.callee.object.type!==`CallExpression`)return 1;let t=e.callee.property,n=e.callee.object;return(t.type===`Identifier`&&c.has(t.name)?1:0)+l(n)}function u(e){if(e.type!==`LogicalExpression`||e.operator!==`&&`)return 0;let t=e.right;return+(t.type===`MemberExpression`||t.type===`LogicalExpression`&&t.operator===`&&`)+u(e.left)}const d={meta:{type:`suggestion`,docs:{description:`Prefer Do notation for complex monadic compositions and nested operations`,recommended:!0},fixable:`code`,messages:{preferDoForNestedChecks:`Prefer Do notation for nested null/undefined checks instead of logical AND chains`,preferDoForChainedMethods:`Prefer Do notation for complex flatMap chains ({{count}} levels deep)`,preferDoForMixedMonads:`Consider Do notation when mixing Option, Either, and Try operations`,preferDoAsyncForChainedTasks:`Prefer DoAsync notation for chained async operations`},schema:[{type:`object`,properties:{minChainDepth:{type:`integer`,minimum:2,default:3},detectMixedMonads:{type:`boolean`,default:!0}},additionalProperties:!1}]},create(n){let r=n.options[0]||{},i=r.minChainDepth||3,a=r.detectMixedMonads!==!1,o=e(n),c=o.functions.has(`Do`)||o.functions.has(`DoAsync`);function d(e){e.operator===`&&`&&u(e)>=2&&f(e)&&n.report({node:e,messageId:`preferDoForNestedChecks`,fix:c?t=>m(t,e):void 0})}function f(e){let t=n.sourceCode.getText(e).match(/\w+(\.\w+){2,}/g);return t!==null&&t.length>0}function p(e){if(!t(e,`flatMap`))return;let r=l(e);r>=i&&n.report({node:e,messageId:`preferDoForChainedMethods`,data:{count:r.toString()},fix:c?t=>h(t,e):void 0})}function m(e,t){let r=n.sourceCode.getText(t),i=r.match(/(\w+)(\.\w+)+/);if(i){let n=i[1],r=`Do(function* () {
1
+ import{getFunctypeImports as e,isChainedMethodCall as t}from"../utils/functype-detection.js";const n=new Set([`Option`,`Either`,`Try`,`Task`]),r=new Set([`parent`,`loc`,`range`]),i=(e,t)=>new Set([...e,...t]);function a(e){if(e.type!==`CallExpression`)return new Set;let t=e.callee;return t?.type===`Identifier`&&n.has(t.name)?new Set([t.name]):t?.type===`MemberExpression`&&t.object?.type===`Identifier`&&n.has(t.object.name)?new Set([t.object.name]):new Set}function o(e){return Object.entries(e).filter(([e])=>!r.has(e)).flatMap(([,e])=>Array.isArray(e)?e:[e]).filter(e=>typeof e==`object`&&!!e)}function s(e){if(!e||typeof e!=`object`)return new Set;let t=e;return o(t).reduce((e,t)=>i(e,s(t)),a(t))}const c=new Set([`flatMap`,`map`,`filter`,`fold`]),l=new Set([`flatMap`,`map`,`filter`]);function u(e){if(e.callee.type!==`MemberExpression`||e.callee.object.type!==`CallExpression`)return 1;let t=e.callee.property,n=e.callee.object;return(t.type===`Identifier`&&c.has(t.name)?1:0)+u(n)}function d(e){if(e.callee?.type!==`MemberExpression`||e.callee.object?.type!==`CallExpression`)return 0;let t=e.callee.property,n=e.callee.object;return(t.type===`Identifier`&&l.has(t.name)?1:0)+d(n)}function f(e){if(e.callee?.type!==`MemberExpression`)return!1;let t=e.callee.property;return t.type!==`Identifier`||!l.has(t.name)?!1:d(e)>=1}function p(e){if(e.type!==`LogicalExpression`||e.operator!==`&&`)return 0;let t=e.right;return+(t.type===`MemberExpression`||t.type===`LogicalExpression`&&t.operator===`&&`)+p(e.left)}const m={meta:{type:`suggestion`,docs:{description:`Prefer Do notation for complex monadic compositions and nested operations`,recommended:!0},fixable:`code`,messages:{preferDoForNestedChecks:`Prefer Do notation for nested null/undefined checks instead of logical AND chains`,preferDoForChainedMethods:`Prefer Do notation for complex flatMap chains ({{count}} levels deep)`,preferDoForMixedMonads:`Consider Do notation when mixing Option, Either, and Try operations`,preferDoAsyncForChainedTasks:`Prefer DoAsync notation for chained async operations`},schema:[{type:`object`,properties:{minChainDepth:{type:`integer`,minimum:2,default:3},detectMixedMonads:{type:`boolean`,default:!0}},additionalProperties:!1}]},create(n){let r=n.options[0]||{},i=r.minChainDepth||3,a=r.detectMixedMonads!==!1,o=e(n),c=o.functions.has(`Do`)||o.functions.has(`DoAsync`);function l(e){e.operator===`&&`&&p(e)>=2&&d(e)&&n.report({node:e,messageId:`preferDoForNestedChecks`,fix:c?t=>h(t,e):void 0})}function d(e){let t=n.sourceCode.getText(e).match(/\w+(\.\w+){2,}/g);return t!==null&&t.length>0}function m(e){if(!t(e,`flatMap`))return;let r=u(e);r>=i&&n.report({node:e,messageId:`preferDoForChainedMethods`,data:{count:r.toString()},fix:c?t=>g(t,e):void 0})}function h(e,t){let r=n.sourceCode.getText(t),i=r.match(/(\w+)(\.\w+)+/);if(i){let n=i[1],r=`Do(function* () {
2
2
  const obj = yield* $(Option(${n}))
3
3
  return yield* $(Option(obj${i[0].substring(n.length)}))
4
- })`;return e.replaceText(t,r)}return e.replaceText(t,`/* TODO: Convert to Do notation */ ${r}`)}function h(e,t){let r=n.sourceCode.getText(t);return e.replaceText(t,`/* TODO: Consider Do notation for complex chains */ ${r}`)}function g(e){a&&s(e).size>=2&&n.report({node:e,messageId:`preferDoForMixedMonads`})}return{LogicalExpression(e){d(e)},CallExpression(e){p(e),g(e)}}}};export{d as default};
4
+ })`;return e.replaceText(t,r)}return e.replaceText(t,`/* TODO: Convert to Do notation */ ${r}`)}function g(e,t){let r=n.sourceCode.getText(t);return e.replaceText(t,`/* TODO: Consider Do notation for complex chains */ ${r}`)}function _(e){a&&f(e)&&s(e).size>=2&&n.report({node:e,messageId:`preferDoForMixedMonads`})}return{LogicalExpression(e){l(e)},CallExpression(e){m(e),_(e)}}}};export{m as default};
5
5
  //# sourceMappingURL=prefer-do-notation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-do-notation.js","names":[],"sources":["../../src/rules/prefer-do-notation.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImports, isChainedMethodCall } from \"../utils/functype-detection\"\n\nconst MONAD_TYPES: ReadonlySet<string> = new Set([\"Option\", \"Either\", \"Try\", \"Task\"])\n\n/** AST keys that don't represent syntax children — back-edges and source metadata. */\nconst NON_CHILD_KEYS: ReadonlySet<string> = new Set([\"parent\", \"loc\", \"range\"])\n\nconst union = <T>(a: ReadonlySet<T>, b: ReadonlySet<T>): ReadonlySet<T> => new Set([...a, ...b])\n\n/**\n * Set of monad names that *this single AST node* names as a constructor call.\n * Does not look at children. Returns empty set for any non-monad-constructor node.\n */\nfunction monadAt(node: ASTNode): ReadonlySet<string> {\n if (node.type !== \"CallExpression\") return new Set()\n const callee = node.callee\n // Bare constructor: Option(...), Try(...), Either(...), Task(...)\n if (callee?.type === \"Identifier\" && MONAD_TYPES.has(callee.name)) {\n return new Set([callee.name])\n }\n // Companion call: Option.none(), Either.right(x), Try.success(v)\n if (\n callee?.type === \"MemberExpression\" &&\n callee.object?.type === \"Identifier\" &&\n MONAD_TYPES.has(callee.object.name)\n ) {\n return new Set([callee.object.name])\n }\n return new Set()\n}\n\n/**\n * AST children of `node` worth recursing into. Drops back-edges and source\n * metadata; flattens array-valued keys (e.g. `arguments`); filters non-object\n * leaves (literals, raw strings, numbers).\n */\nfunction astChildren(node: ASTNode): readonly unknown[] {\n return Object.entries(node)\n .filter(([k]) => !NON_CHILD_KEYS.has(k))\n .flatMap(([, v]) => (Array.isArray(v) ? v : [v]))\n .filter((v) => v !== null && typeof v === \"object\")\n}\n\n/**\n * Pure recursion: the set of all monad names reachable from `node` (including\n * `node` itself if it is a monad constructor call). No mutation, no\n * accumulator parameter — just a fold over the children's results.\n */\nfunction monadsIn(node: unknown): ReadonlySet<string> {\n if (!node || typeof node !== \"object\") return new Set()\n const ast = node as ASTNode\n return astChildren(ast).reduce<ReadonlySet<string>>((acc, child) => union(acc, monadsIn(child)), monadAt(ast))\n}\n\nconst CHAIN_METHODS: ReadonlySet<string> = new Set([\"flatMap\", \"map\", \"filter\", \"fold\"])\n\n/**\n * Length of the chain of `.flatMap()/.map()/.filter()/.fold()` calls walking\n * left from `node`. Returns 1 for a single call, N for N stacked calls. Pure\n * tail recursion — no mutable counter, no `while` loop.\n */\nfunction getChainDepth(node: ASTNode): number {\n if (node.callee.type !== \"MemberExpression\" || node.callee.object.type !== \"CallExpression\") return 1\n const method = node.callee.property\n const next = node.callee.object as ASTNode\n const here = method.type === \"Identifier\" && CHAIN_METHODS.has(method.name) ? 1 : 0\n return here + getChainDepth(next)\n}\n\n/**\n * Depth of a logical-AND chain (`a && b && c && ...`) where each right-hand\n * side is either a MemberExpression or another && chain — the shape that\n * indicates nested property-access guarding rather than arbitrary booleans.\n */\nfunction andChainDepth(node: ASTNode): number {\n if (node.type !== \"LogicalExpression\" || node.operator !== \"&&\") return 0\n const rhs = node.right\n const here = rhs.type === \"MemberExpression\" || (rhs.type === \"LogicalExpression\" && rhs.operator === \"&&\") ? 1 : 0\n return here + andChainDepth(node.left as ASTNode)\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer Do notation for complex monadic compositions and nested operations\",\n recommended: true,\n },\n fixable: \"code\",\n messages: {\n preferDoForNestedChecks: \"Prefer Do notation for nested null/undefined checks instead of logical AND chains\",\n preferDoForChainedMethods: \"Prefer Do notation for complex flatMap chains ({{count}} levels deep)\",\n preferDoForMixedMonads: \"Consider Do notation when mixing Option, Either, and Try operations\",\n preferDoAsyncForChainedTasks: \"Prefer DoAsync notation for chained async operations\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n minChainDepth: {\n type: \"integer\",\n minimum: 2,\n default: 3,\n },\n detectMixedMonads: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n create(context) {\n const options = context.options[0] || {}\n const minChainDepth = options.minChainDepth || 3\n const detectMixedMonads = options.detectMixedMonads !== false\n\n const functypeImports = getFunctypeImports(context)\n\n // Track if Do notation is already imported\n const hasDoImport = functypeImports.functions.has(\"Do\") || functypeImports.functions.has(\"DoAsync\")\n\n /**\n * Detect nested null/undefined checks like: a && a.b && a.b.c\n */\n function checkNestedNullChecks(node: ASTNode): void {\n if (node.operator !== \"&&\") return\n\n // Check if this looks like nested property access guarding\n if (andChainDepth(node) >= 2 && hasNestedPropertyAccess(node)) {\n context.report({\n node,\n messageId: \"preferDoForNestedChecks\",\n fix: hasDoImport ? (fixer) => fixNestedChecks(fixer, node) : undefined,\n })\n }\n }\n\n /**\n * Check if logical expression contains nested property access patterns\n */\n function hasNestedPropertyAccess(node: ASTNode): boolean {\n // Look for patterns like: obj && obj.prop && obj.prop.nested\n const matches = context.sourceCode.getText(node).match(/\\w+(\\.\\w+){2,}/g)\n return matches !== null && matches.length > 0\n }\n\n /**\n * Detect long flatMap chains\n */\n function checkChainedMethods(node: ASTNode): void {\n if (!isChainedMethodCall(node, \"flatMap\")) return\n\n const chainDepth = getChainDepth(node)\n\n if (chainDepth >= minChainDepth) {\n context.report({\n node,\n messageId: \"preferDoForChainedMethods\",\n data: { count: chainDepth.toString() },\n fix: hasDoImport ? (fixer) => fixChainedMethods(fixer, node) : undefined,\n })\n }\n }\n\n /**\n * Auto-fix for nested null checks\n */\n function fixNestedChecks(fixer: Rule.RuleFixer, node: ASTNode): Rule.Fix {\n const text = context.sourceCode.getText(node)\n\n // Simple transformation for common patterns\n // a && a.b && a.b.c -> Do(function* () { const x = yield* $(Option(a)); const y = yield* $(Option(x.b)); return Option(y.c) })\n const match = text.match(/(\\w+)(\\.\\w+)+/)\n if (match) {\n const baseVar = match[1]\n const chain = match[0]\n\n const doNotation = `Do(function* () {\n const obj = yield* $(Option(${baseVar}))\n return yield* $(Option(obj${chain.substring(baseVar.length)}))\n})`\n\n return fixer.replaceText(node, doNotation)\n }\n\n return fixer.replaceText(node, `/* TODO: Convert to Do notation */ ${text}`)\n }\n\n /**\n * Auto-fix for chained methods (basic)\n */\n function fixChainedMethods(fixer: Rule.RuleFixer, node: ASTNode): Rule.Fix {\n const text = context.sourceCode.getText(node)\n\n // For now, just add a comment suggesting Do notation\n return fixer.replaceText(node, `/* TODO: Consider Do notation for complex chains */ ${text}`)\n }\n\n /**\n * Detect mixed monad usage by walking the AST under `node` and counting\n * *distinct* monad constructor calls — not substring matches.\n *\n * A \"monad constructor call\" is a CallExpression whose callee is either:\n * - a bare identifier matching a known monad type (`Option(...)`, `Try(...)`),\n * or\n * - a member expression whose object is a known monad type\n * (`Option.none()`, `Either.right(...)`, `Try.success(...)`).\n *\n * Method calls like `.toEither(...)`, `.toOption()`, `.toTry()` are NOT\n * constructor calls — they're the idiomatic *conversion* API for crossing\n * monad boundaries. The old substring-based check fired on these because\n * `.toEither` contains \"Either\" as a substring; the AST check correctly\n * sees them as method calls, not new monad constructions.\n *\n * Implementation is pure recursion over the AST: `monadAt` reports what\n * a single node names; `monadsIn` returns the union of `monadAt(self)`\n * with the results from each child. No mutable accumulator, no for-loops\n * driving state changes.\n */\n function checkMixedMonads(node: ASTNode): void {\n if (!detectMixedMonads) return\n if (monadsIn(node).size >= 2) {\n context.report({ node, messageId: \"preferDoForMixedMonads\" })\n }\n }\n\n return {\n LogicalExpression(node) {\n checkNestedNullChecks(node)\n },\n\n CallExpression(node) {\n checkChainedMethods(node)\n checkMixedMonads(node)\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
1
+ {"version":3,"file":"prefer-do-notation.js","names":[],"sources":["../../src/rules/prefer-do-notation.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImports, isChainedMethodCall } from \"../utils/functype-detection\"\n\nconst MONAD_TYPES: ReadonlySet<string> = new Set([\"Option\", \"Either\", \"Try\", \"Task\"])\n\n/** AST keys that don't represent syntax children — back-edges and source metadata. */\nconst NON_CHILD_KEYS: ReadonlySet<string> = new Set([\"parent\", \"loc\", \"range\"])\n\nconst union = <T>(a: ReadonlySet<T>, b: ReadonlySet<T>): ReadonlySet<T> => new Set([...a, ...b])\n\n/**\n * Set of monad names that *this single AST node* names as a constructor call.\n * Does not look at children. Returns empty set for any non-monad-constructor node.\n */\nfunction monadAt(node: ASTNode): ReadonlySet<string> {\n if (node.type !== \"CallExpression\") return new Set()\n const callee = node.callee\n // Bare constructor: Option(...), Try(...), Either(...), Task(...)\n if (callee?.type === \"Identifier\" && MONAD_TYPES.has(callee.name)) {\n return new Set([callee.name])\n }\n // Companion call: Option.none(), Either.right(x), Try.success(v)\n if (\n callee?.type === \"MemberExpression\" &&\n callee.object?.type === \"Identifier\" &&\n MONAD_TYPES.has(callee.object.name)\n ) {\n return new Set([callee.object.name])\n }\n return new Set()\n}\n\n/**\n * AST children of `node` worth recursing into. Drops back-edges and source\n * metadata; flattens array-valued keys (e.g. `arguments`); filters non-object\n * leaves (literals, raw strings, numbers).\n */\nfunction astChildren(node: ASTNode): readonly unknown[] {\n return Object.entries(node)\n .filter(([k]) => !NON_CHILD_KEYS.has(k))\n .flatMap(([, v]) => (Array.isArray(v) ? v : [v]))\n .filter((v) => v !== null && typeof v === \"object\")\n}\n\n/**\n * Pure recursion: the set of all monad names reachable from `node` (including\n * `node` itself if it is a monad constructor call). No mutation, no\n * accumulator parameter — just a fold over the children's results.\n */\nfunction monadsIn(node: unknown): ReadonlySet<string> {\n if (!node || typeof node !== \"object\") return new Set()\n const ast = node as ASTNode\n return astChildren(ast).reduce<ReadonlySet<string>>((acc, child) => union(acc, monadsIn(child)), monadAt(ast))\n}\n\nconst CHAIN_METHODS: ReadonlySet<string> = new Set([\"flatMap\", \"map\", \"filter\", \"fold\"])\n\n/**\n * Methods that *compose* monadic operations (stay in the monad and feed the\n * next step). Distinct from terminal handlers (`fold`, `match`, `getOrElse`)\n * which *exit* the monad — those are conversions, not composition, and Do\n * notation doesn't apply to them.\n */\nconst COMPOSITION_METHODS: ReadonlySet<string> = new Set([\"flatMap\", \"map\", \"filter\"])\n\n/**\n * Length of the chain of `.flatMap()/.map()/.filter()/.fold()` calls walking\n * left from `node`. Returns 1 for a single call, N for N stacked calls. Pure\n * tail recursion — no mutable counter, no `while` loop.\n */\nfunction getChainDepth(node: ASTNode): number {\n if (node.callee.type !== \"MemberExpression\" || node.callee.object.type !== \"CallExpression\") return 1\n const method = node.callee.property\n const next = node.callee.object as ASTNode\n const here = method.type === \"Identifier\" && CHAIN_METHODS.has(method.name) ? 1 : 0\n return here + getChainDepth(next)\n}\n\n/**\n * Number of composition methods (`flatMap`/`map`/`filter`) stacked under\n * `node` via `.callee.object`. 0 if the callee isn't a member expression on\n * another call. Excludes terminal handlers by design.\n */\nfunction compositionChainDepth(node: ASTNode): number {\n if (node.callee?.type !== \"MemberExpression\" || node.callee.object?.type !== \"CallExpression\") return 0\n const method = node.callee.property\n const next = node.callee.object as ASTNode\n const here = method.type === \"Identifier\" && COMPOSITION_METHODS.has(method.name) ? 1 : 0\n return here + compositionChainDepth(next)\n}\n\n/**\n * True when `node`'s outermost call is a composition method AND at least one\n * more composition method appears below it — i.e. this is a real\n * `.flatMap(...).flatMap(...)`-style chain, not a terminal exit like\n * `.fold(...)` or a bare receiver like `option.flatMap(...)`.\n */\nfunction isCompositionChain(node: ASTNode): boolean {\n if (node.callee?.type !== \"MemberExpression\") return false\n const method = node.callee.property\n if (method.type !== \"Identifier\" || !COMPOSITION_METHODS.has(method.name)) return false\n return compositionChainDepth(node) >= 1\n}\n\n/**\n * Depth of a logical-AND chain (`a && b && c && ...`) where each right-hand\n * side is either a MemberExpression or another && chain — the shape that\n * indicates nested property-access guarding rather than arbitrary booleans.\n */\nfunction andChainDepth(node: ASTNode): number {\n if (node.type !== \"LogicalExpression\" || node.operator !== \"&&\") return 0\n const rhs = node.right\n const here = rhs.type === \"MemberExpression\" || (rhs.type === \"LogicalExpression\" && rhs.operator === \"&&\") ? 1 : 0\n return here + andChainDepth(node.left as ASTNode)\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer Do notation for complex monadic compositions and nested operations\",\n recommended: true,\n },\n fixable: \"code\",\n messages: {\n preferDoForNestedChecks: \"Prefer Do notation for nested null/undefined checks instead of logical AND chains\",\n preferDoForChainedMethods: \"Prefer Do notation for complex flatMap chains ({{count}} levels deep)\",\n preferDoForMixedMonads: \"Consider Do notation when mixing Option, Either, and Try operations\",\n preferDoAsyncForChainedTasks: \"Prefer DoAsync notation for chained async operations\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n minChainDepth: {\n type: \"integer\",\n minimum: 2,\n default: 3,\n },\n detectMixedMonads: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n create(context) {\n const options = context.options[0] || {}\n const minChainDepth = options.minChainDepth || 3\n const detectMixedMonads = options.detectMixedMonads !== false\n\n const functypeImports = getFunctypeImports(context)\n\n // Track if Do notation is already imported\n const hasDoImport = functypeImports.functions.has(\"Do\") || functypeImports.functions.has(\"DoAsync\")\n\n /**\n * Detect nested null/undefined checks like: a && a.b && a.b.c\n */\n function checkNestedNullChecks(node: ASTNode): void {\n if (node.operator !== \"&&\") return\n\n // Check if this looks like nested property access guarding\n if (andChainDepth(node) >= 2 && hasNestedPropertyAccess(node)) {\n context.report({\n node,\n messageId: \"preferDoForNestedChecks\",\n fix: hasDoImport ? (fixer) => fixNestedChecks(fixer, node) : undefined,\n })\n }\n }\n\n /**\n * Check if logical expression contains nested property access patterns\n */\n function hasNestedPropertyAccess(node: ASTNode): boolean {\n // Look for patterns like: obj && obj.prop && obj.prop.nested\n const matches = context.sourceCode.getText(node).match(/\\w+(\\.\\w+){2,}/g)\n return matches !== null && matches.length > 0\n }\n\n /**\n * Detect long flatMap chains\n */\n function checkChainedMethods(node: ASTNode): void {\n if (!isChainedMethodCall(node, \"flatMap\")) return\n\n const chainDepth = getChainDepth(node)\n\n if (chainDepth >= minChainDepth) {\n context.report({\n node,\n messageId: \"preferDoForChainedMethods\",\n data: { count: chainDepth.toString() },\n fix: hasDoImport ? (fixer) => fixChainedMethods(fixer, node) : undefined,\n })\n }\n }\n\n /**\n * Auto-fix for nested null checks\n */\n function fixNestedChecks(fixer: Rule.RuleFixer, node: ASTNode): Rule.Fix {\n const text = context.sourceCode.getText(node)\n\n // Simple transformation for common patterns\n // a && a.b && a.b.c -> Do(function* () { const x = yield* $(Option(a)); const y = yield* $(Option(x.b)); return Option(y.c) })\n const match = text.match(/(\\w+)(\\.\\w+)+/)\n if (match) {\n const baseVar = match[1]\n const chain = match[0]\n\n const doNotation = `Do(function* () {\n const obj = yield* $(Option(${baseVar}))\n return yield* $(Option(obj${chain.substring(baseVar.length)}))\n})`\n\n return fixer.replaceText(node, doNotation)\n }\n\n return fixer.replaceText(node, `/* TODO: Convert to Do notation */ ${text}`)\n }\n\n /**\n * Auto-fix for chained methods (basic)\n */\n function fixChainedMethods(fixer: Rule.RuleFixer, node: ASTNode): Rule.Fix {\n const text = context.sourceCode.getText(node)\n\n // For now, just add a comment suggesting Do notation\n return fixer.replaceText(node, `/* TODO: Consider Do notation for complex chains */ ${text}`)\n }\n\n /**\n * Detect mixed monad usage by walking the AST under `node` and counting\n * *distinct* monad constructor calls — not substring matches.\n *\n * A \"monad constructor call\" is a CallExpression whose callee is either:\n * - a bare identifier matching a known monad type (`Option(...)`, `Try(...)`),\n * or\n * - a member expression whose object is a known monad type\n * (`Option.none()`, `Either.right(...)`, `Try.success(...)`).\n *\n * Method calls like `.toEither(...)`, `.toOption()`, `.toTry()` are NOT\n * constructor calls — they're the idiomatic *conversion* API for crossing\n * monad boundaries. The old substring-based check fired on these because\n * `.toEither` contains \"Either\" as a substring; the AST check correctly\n * sees them as method calls, not new monad constructions.\n *\n * Implementation is pure recursion over the AST: `monadAt` reports what\n * a single node names; `monadsIn` returns the union of `monadAt(self)`\n * with the results from each child. No mutable accumulator, no for-loops\n * driving state changes.\n */\n function checkMixedMonads(node: ASTNode): void {\n if (!detectMixedMonads) return\n // Only flag genuine composition chains (.flatMap/.map/.filter stacked at\n // least twice deep). Terminal handlers (.fold/.match/.getOrElse) and\n // bare co-occurrence in arg lists are conversions, not compositions —\n // Do notation doesn't apply. See issue #205.\n if (!isCompositionChain(node)) return\n if (monadsIn(node).size >= 2) {\n context.report({ node, messageId: \"preferDoForMixedMonads\" })\n }\n }\n\n return {\n LogicalExpression(node) {\n checkNestedNullChecks(node)\n },\n\n CallExpression(node) {\n checkChainedMethods(node)\n checkMixedMonads(node)\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{createImportFixer as e,hasFunctypeSymbol as t}from"../utils/import-fixer.js";const n={meta:{type:`suggestion`,hasSuggestions:!0,docs:{description:`Prefer Try for computations that may throw`,recommended:!0},schema:[{type:`object`,properties:{allowThrowInTests:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferTryOverTryCatch:`Prefer Try(() => ...) over try/catch block`,suggestTry:`Replace with Try(() => ...)`,suggestTryFromPromise:`Replace with Try.fromPromise(...)`,suggestAddImport:`Add {{symbol}} import from functype`}},create(n){let r=(n.options[0]||{}).allowThrowInTests!==!1;function i(){let e=n.filename;return/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(e)||e.includes(`__tests__`)||e.includes(`/test/`)||e.includes(`/tests/`)}function a(e){return e.block?.body?.length===1}function o(e){if(!e.handler?.body)return!1;let t=e.handler.body.body;return t.length===0||t.length===1&&(t[0].type===`ReturnStatement`||t[0].type===`ExpressionStatement`)}function s(e){let t=e.block.body[0];return t.type===`ReturnStatement`&&t.argument?.type===`AwaitExpression`||t.type===`ExpressionStatement`&&t.expression?.type===`AwaitExpression`}return{TryStatement(c){if(r&&i()||c.handler&&c.handler.body&&c.handler.body.body.some(e=>e.type===`ThrowStatement`))return;let l=n.sourceCode,u=[];if(a(c)&&o(c)){let n=c.block.body[0];if(n.type===`ReturnStatement`){let r=n.argument,i=l.getText(r);if(s(c)){let e=r.type===`AwaitExpression`?r.argument:r,t=l.getText(e);u.push({messageId:`suggestTryFromPromise`,fix(e){return e.replaceText(c,`return Try.fromPromise(${t})`)}})}else u.push({messageId:`suggestTry`,fix(e){return e.replaceText(c,`return Try(() => ${i})`)}});t(l,`Try`)||u.push({messageId:`suggestAddImport`,data:{symbol:`Try`},fix:e(l,`Try`)})}}n.report({node:c,messageId:`preferTryOverTryCatch`,suggest:u})}}}};export{n as default};
1
+ import{createImportFixer as e,hasFunctypeSymbol as t}from"../utils/import-fixer.js";const n={meta:{type:`suggestion`,hasSuggestions:!0,docs:{description:`Prefer Try for computations that may throw`,recommended:!0},schema:[{type:`object`,properties:{allowThrowInTests:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferTryOverTryCatch:`Prefer Try(() => ...) over try/catch block`,suggestTry:`Replace with Try(() => ...)`,suggestTryFromPromise:`Replace with Try.fromPromise(...)`,suggestAddImport:`Add {{symbol}} import from functype`}},create(n){let r=(n.options[0]||{}).allowThrowInTests!==!1;function i(){let e=n.filename;return/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(e)||e.includes(`__tests__`)||e.includes(`/test/`)||e.includes(`/tests/`)}function a(e){return e.block?.body?.length===1}function o(e){if(!e.handler?.body)return!1;let t=e.handler.body.body;return t.length===0||t.length===1&&(t[0].type===`ReturnStatement`||t[0].type===`ExpressionStatement`)}function s(e){let t=e.block.body[0];return t.type===`ReturnStatement`&&t.argument?.type===`AwaitExpression`||t.type===`ExpressionStatement`&&t.expression?.type===`AwaitExpression`}return{TryStatement(c){if(c.finalizer||r&&i()||c.handler&&c.handler.body&&c.handler.body.body.some(e=>e.type===`ThrowStatement`))return;let l=n.sourceCode,u=[];if(a(c)&&o(c)){let n=c.block.body[0];if(n.type===`ReturnStatement`){let r=n.argument,i=l.getText(r);if(s(c)){let e=r.type===`AwaitExpression`?r.argument:r,t=l.getText(e);u.push({messageId:`suggestTryFromPromise`,fix(e){return e.replaceText(c,`return Try.fromPromise(${t})`)}})}else u.push({messageId:`suggestTry`,fix(e){return e.replaceText(c,`return Try(() => ${i})`)}});t(l,`Try`)||u.push({messageId:`suggestAddImport`,data:{symbol:`Try`},fix:e(l,`Try`)})}}n.report({node:c,messageId:`preferTryOverTryCatch`,suggest:u})}}}};export{n as default};
2
2
  //# sourceMappingURL=prefer-try.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-try.js","names":[],"sources":["../../src/rules/prefer-try.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { createImportFixer, hasFunctypeSymbol } from \"../utils/import-fixer\"\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n hasSuggestions: true,\n docs: {\n description: \"Prefer Try for computations that may throw\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowThrowInTests: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferTryOverTryCatch: \"Prefer Try(() => ...) over try/catch block\",\n suggestTry: \"Replace with Try(() => ...)\",\n suggestTryFromPromise: \"Replace with Try.fromPromise(...)\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const allowThrowInTests = options.allowThrowInTests !== false\n\n function isInTestFile() {\n const filename = context.filename\n return (\n /\\.(test|spec)\\.(ts|js|tsx|jsx)$/.test(filename) ||\n filename.includes(\"__tests__\") ||\n filename.includes(\"/test/\") ||\n filename.includes(\"/tests/\")\n )\n }\n\n function isSimpleTryBody(node: ASTNode): boolean {\n return node.block?.body?.length === 1\n }\n\n function isSimpleCatch(node: ASTNode): boolean {\n if (!node.handler?.body) return false\n const catchBody = node.handler.body.body\n return (\n catchBody.length === 0 ||\n (catchBody.length === 1 &&\n (catchBody[0].type === \"ReturnStatement\" || catchBody[0].type === \"ExpressionStatement\"))\n )\n }\n\n function tryBodyHasAwait(node: ASTNode): boolean {\n const stmt = node.block.body[0]\n if (stmt.type === \"ReturnStatement\" && stmt.argument?.type === \"AwaitExpression\") return true\n if (stmt.type === \"ExpressionStatement\" && stmt.expression?.type === \"AwaitExpression\") return true\n return false\n }\n\n return {\n TryStatement(node: ASTNode) {\n // Allow try/catch in test files\n if (allowThrowInTests && isInTestFile()) return\n\n // Allow try/catch that re-throws in the catch block (even with logging)\n if (node.handler && node.handler.body) {\n const catchBody = node.handler.body.body\n const hasRethrow = catchBody.some((stmt: ASTNode) => stmt.type === \"ThrowStatement\")\n if (hasRethrow) return\n }\n\n const sourceCode = context.sourceCode\n const suggest: Rule.SuggestionReportDescriptor[] = []\n\n if (isSimpleTryBody(node) && isSimpleCatch(node)) {\n const tryStmt = node.block.body[0]\n const isReturn = tryStmt.type === \"ReturnStatement\"\n\n // Only suggest when the try body is a return statement — non-return expression\n // replacements would produce syntactically ambiguous code without knowing the context\n if (isReturn) {\n const expr = tryStmt.argument\n const exprText = sourceCode.getText(expr)\n\n if (tryBodyHasAwait(node)) {\n const awaitExpr = expr.type === \"AwaitExpression\" ? expr.argument : expr\n const innerText = sourceCode.getText(awaitExpr)\n suggest.push({\n messageId: \"suggestTryFromPromise\",\n fix(fixer) {\n return fixer.replaceText(node, `return Try.fromPromise(${innerText})`)\n },\n })\n } else {\n suggest.push({\n messageId: \"suggestTry\",\n fix(fixer) {\n return fixer.replaceText(node, `return Try(() => ${exprText})`)\n },\n })\n }\n\n if (!hasFunctypeSymbol(sourceCode, \"Try\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Try\" },\n fix: createImportFixer(sourceCode, \"Try\"),\n })\n }\n }\n }\n\n context.report({\n node,\n messageId: \"preferTryOverTryCatch\",\n suggest,\n })\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"oFAKA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,6CACb,YAAa,EACf,EACA,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,kBAAmB,CACjB,KAAM,UACN,QAAS,EACX,CACF,EACA,qBAAsB,EACxB,CACF,EACA,SAAU,CACR,sBAAuB,6CACvB,WAAY,8BACZ,sBAAuB,oCACvB,iBAAkB,qCACpB,CACF,EAEA,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,CAAC,EAAA,CACL,oBAAsB,GAExD,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,CAAQ,GAC/C,EAAS,SAAS,WAAW,GAC7B,EAAS,SAAS,QAAQ,GAC1B,EAAS,SAAS,SAAS,CAE/B,CAEA,SAAS,EAAgB,EAAwB,CAC/C,OAAO,EAAK,OAAO,MAAM,SAAW,CACtC,CAEA,SAAS,EAAc,EAAwB,CAC7C,GAAI,CAAC,EAAK,SAAS,KAAM,MAAO,GAChC,IAAM,EAAY,EAAK,QAAQ,KAAK,KACpC,OACE,EAAU,SAAW,GACpB,EAAU,SAAW,IACnB,EAAU,EAAE,CAAC,OAAS,mBAAqB,EAAU,EAAE,CAAC,OAAS,sBAExE,CAEA,SAAS,EAAgB,EAAwB,CAC/C,IAAM,EAAO,EAAK,MAAM,KAAK,GAG7B,OAFI,EAAK,OAAS,mBAAqB,EAAK,UAAU,OAAS,mBAC3D,EAAK,OAAS,uBAAyB,EAAK,YAAY,OAAS,iBAEvE,CAEA,MAAO,CACL,aAAa,EAAe,CAK1B,GAHI,GAAqB,EAAa,GAGlC,EAAK,SAAW,EAAK,QAAQ,MACb,EAAK,QAAQ,KAAK,KACP,KAAM,GAAkB,EAAK,OAAS,gBACtD,EAAG,OAGlB,IAAM,EAAa,EAAQ,WACrB,EAA6C,CAAC,EAEpD,GAAI,EAAgB,CAAI,GAAK,EAAc,CAAI,EAAG,CAChD,IAAM,EAAU,EAAK,MAAM,KAAK,GAKhC,GAJiB,EAAQ,OAAS,kBAIpB,CACZ,IAAM,EAAO,EAAQ,SACf,EAAW,EAAW,QAAQ,CAAI,EAExC,GAAI,EAAgB,CAAI,EAAG,CACzB,IAAM,EAAY,EAAK,OAAS,kBAAoB,EAAK,SAAW,EAC9D,EAAY,EAAW,QAAQ,CAAS,EAC9C,EAAQ,KAAK,CACX,UAAW,wBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,0BAA0B,EAAU,EAAE,CACvE,CACF,CAAC,CACH,MACE,EAAQ,KAAK,CACX,UAAW,aACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,oBAAoB,EAAS,EAAE,CAChE,CACF,CAAC,EAGE,EAAkB,EAAY,KAAK,GACtC,EAAQ,KAAK,CACX,UAAW,mBACX,KAAM,CAAE,OAAQ,KAAM,EACtB,IAAK,EAAkB,EAAY,KAAK,CAC1C,CAAC,CAEL,CACF,CAEA,EAAQ,OAAO,CACb,OACA,UAAW,wBACX,SACF,CAAC,CACH,CACF,CACF,CACF"}
1
+ {"version":3,"file":"prefer-try.js","names":[],"sources":["../../src/rules/prefer-try.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { createImportFixer, hasFunctypeSymbol } from \"../utils/import-fixer\"\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n hasSuggestions: true,\n docs: {\n description: \"Prefer Try for computations that may throw\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowThrowInTests: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferTryOverTryCatch: \"Prefer Try(() => ...) over try/catch block\",\n suggestTry: \"Replace with Try(() => ...)\",\n suggestTryFromPromise: \"Replace with Try.fromPromise(...)\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const allowThrowInTests = options.allowThrowInTests !== false\n\n function isInTestFile() {\n const filename = context.filename\n return (\n /\\.(test|spec)\\.(ts|js|tsx|jsx)$/.test(filename) ||\n filename.includes(\"__tests__\") ||\n filename.includes(\"/test/\") ||\n filename.includes(\"/tests/\")\n )\n }\n\n function isSimpleTryBody(node: ASTNode): boolean {\n return node.block?.body?.length === 1\n }\n\n function isSimpleCatch(node: ASTNode): boolean {\n if (!node.handler?.body) return false\n const catchBody = node.handler.body.body\n return (\n catchBody.length === 0 ||\n (catchBody.length === 1 &&\n (catchBody[0].type === \"ReturnStatement\" || catchBody[0].type === \"ExpressionStatement\"))\n )\n }\n\n function tryBodyHasAwait(node: ASTNode): boolean {\n const stmt = node.block.body[0]\n if (stmt.type === \"ReturnStatement\" && stmt.argument?.type === \"AwaitExpression\") return true\n if (stmt.type === \"ExpressionStatement\" && stmt.expression?.type === \"AwaitExpression\") return true\n return false\n }\n\n return {\n TryStatement(node: ASTNode) {\n // Try cannot model `finally` — bail on any try statement with a finalizer.\n // Covers two bugs: (1) catch-less try/finally fires a misleading warning\n // with no useful autofix; (2) try/catch/finally autofix would lift the\n // catch into Try(() => …) and silently drop the finally clause. The FP\n // primitive for guaranteed cleanup is IO.bracket, on the lazy IO type.\n // See #206.\n if (node.finalizer) return\n\n // Allow try/catch in test files\n if (allowThrowInTests && isInTestFile()) return\n\n // Allow try/catch that re-throws in the catch block (even with logging)\n if (node.handler && node.handler.body) {\n const catchBody = node.handler.body.body\n const hasRethrow = catchBody.some((stmt: ASTNode) => stmt.type === \"ThrowStatement\")\n if (hasRethrow) return\n }\n\n const sourceCode = context.sourceCode\n const suggest: Rule.SuggestionReportDescriptor[] = []\n\n if (isSimpleTryBody(node) && isSimpleCatch(node)) {\n const tryStmt = node.block.body[0]\n const isReturn = tryStmt.type === \"ReturnStatement\"\n\n // Only suggest when the try body is a return statement — non-return expression\n // replacements would produce syntactically ambiguous code without knowing the context\n if (isReturn) {\n const expr = tryStmt.argument\n const exprText = sourceCode.getText(expr)\n\n if (tryBodyHasAwait(node)) {\n const awaitExpr = expr.type === \"AwaitExpression\" ? expr.argument : expr\n const innerText = sourceCode.getText(awaitExpr)\n suggest.push({\n messageId: \"suggestTryFromPromise\",\n fix(fixer) {\n return fixer.replaceText(node, `return Try.fromPromise(${innerText})`)\n },\n })\n } else {\n suggest.push({\n messageId: \"suggestTry\",\n fix(fixer) {\n return fixer.replaceText(node, `return Try(() => ${exprText})`)\n },\n })\n }\n\n if (!hasFunctypeSymbol(sourceCode, \"Try\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Try\" },\n fix: createImportFixer(sourceCode, \"Try\"),\n })\n }\n }\n }\n\n context.report({\n node,\n messageId: \"preferTryOverTryCatch\",\n suggest,\n })\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"oFAKA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,6CACb,YAAa,EACf,EACA,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,kBAAmB,CACjB,KAAM,UACN,QAAS,EACX,CACF,EACA,qBAAsB,EACxB,CACF,EACA,SAAU,CACR,sBAAuB,6CACvB,WAAY,8BACZ,sBAAuB,oCACvB,iBAAkB,qCACpB,CACF,EAEA,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,CAAC,EAAA,CACL,oBAAsB,GAExD,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,CAAQ,GAC/C,EAAS,SAAS,WAAW,GAC7B,EAAS,SAAS,QAAQ,GAC1B,EAAS,SAAS,SAAS,CAE/B,CAEA,SAAS,EAAgB,EAAwB,CAC/C,OAAO,EAAK,OAAO,MAAM,SAAW,CACtC,CAEA,SAAS,EAAc,EAAwB,CAC7C,GAAI,CAAC,EAAK,SAAS,KAAM,MAAO,GAChC,IAAM,EAAY,EAAK,QAAQ,KAAK,KACpC,OACE,EAAU,SAAW,GACpB,EAAU,SAAW,IACnB,EAAU,EAAE,CAAC,OAAS,mBAAqB,EAAU,EAAE,CAAC,OAAS,sBAExE,CAEA,SAAS,EAAgB,EAAwB,CAC/C,IAAM,EAAO,EAAK,MAAM,KAAK,GAG7B,OAFI,EAAK,OAAS,mBAAqB,EAAK,UAAU,OAAS,mBAC3D,EAAK,OAAS,uBAAyB,EAAK,YAAY,OAAS,iBAEvE,CAEA,MAAO,CACL,aAAa,EAAe,CAa1B,GANI,EAAK,WAGL,GAAqB,EAAa,GAGlC,EAAK,SAAW,EAAK,QAAQ,MACb,EAAK,QAAQ,KAAK,KACP,KAAM,GAAkB,EAAK,OAAS,gBACtD,EAAG,OAGlB,IAAM,EAAa,EAAQ,WACrB,EAA6C,CAAC,EAEpD,GAAI,EAAgB,CAAI,GAAK,EAAc,CAAI,EAAG,CAChD,IAAM,EAAU,EAAK,MAAM,KAAK,GAKhC,GAJiB,EAAQ,OAAS,kBAIpB,CACZ,IAAM,EAAO,EAAQ,SACf,EAAW,EAAW,QAAQ,CAAI,EAExC,GAAI,EAAgB,CAAI,EAAG,CACzB,IAAM,EAAY,EAAK,OAAS,kBAAoB,EAAK,SAAW,EAC9D,EAAY,EAAW,QAAQ,CAAS,EAC9C,EAAQ,KAAK,CACX,UAAW,wBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,0BAA0B,EAAU,EAAE,CACvE,CACF,CAAC,CACH,MACE,EAAQ,KAAK,CACX,UAAW,aACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,oBAAoB,EAAS,EAAE,CAChE,CACF,CAAC,EAGE,EAAkB,EAAY,KAAK,GACtC,EAAQ,KAAK,CACX,UAAW,mBACX,KAAM,CAAE,OAAQ,KAAM,EACtB,IAAK,EAAkB,EAAY,KAAK,CAC1C,CAAC,CAEL,CACF,CAEA,EAAQ,OAAO,CACb,OACA,UAAW,wBACX,SACF,CAAC,CACH,CACF,CACF,CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-functype",
3
- "version": "2.104.1",
3
+ "version": "2.104.3",
4
4
  "description": "Custom ESLint rules for functional TypeScript programming with functype library patterns including Do notation (ESLint 10+)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,11 +40,11 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^24.13.2",
43
- "@typescript-eslint/rule-tester": "^8.61.0",
43
+ "@typescript-eslint/rule-tester": "^8.61.1",
44
44
  "eslint-config-prettier": "^10.1.8",
45
- "ts-builds": "^3.1.0",
45
+ "ts-builds": "^3.2.0",
46
46
  "tsdown": "^0.22.2",
47
- "functype": "^1.4.1"
47
+ "functype": "^1.4.3"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public",