eslint-plugin-functype 2.104.1 → 2.104.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -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
|
|
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
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-functype",
|
|
3
|
-
"version": "2.104.
|
|
3
|
+
"version": "2.104.2",
|
|
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.
|
|
43
|
+
"@typescript-eslint/rule-tester": "^8.61.1",
|
|
44
44
|
"eslint-config-prettier": "^10.1.8",
|
|
45
|
-
"ts-builds": "^3.
|
|
45
|
+
"ts-builds": "^3.2.0",
|
|
46
46
|
"tsdown": "^0.22.2",
|
|
47
|
-
"functype": "^1.4.
|
|
47
|
+
"functype": "^1.4.2"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public",
|