eslint-plugin-functype 2.106.2 → 2.106.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,2 +1,2 @@
|
|
|
1
|
-
const e=new Set([`isSome`,`isRight`,`isSuccess`]),t=new Set([`isNone`,`isEmpty`,`isLeft`,`isFailure`]);function n(n,r){if(n.type!==`CallExpression`||n.callee.type!==`MemberExpression`)return null;let i=n.callee.property.name,a=r.getText(n.callee.object);return e.has(i)?{obj:a,isNegated:!1}:t.has(i)?{obj:a,isNegated:!0}:null}function
|
|
1
|
+
const e=new Set([`isSome`,`isRight`,`isSuccess`]),t=new Set([`isNone`,`isEmpty`,`isLeft`,`isFailure`]);function n(e){return e.type===`Literal`&&e.value===null||e.type===`Identifier`&&e.name===`undefined`}function r(n,r){if(n.type!==`CallExpression`||n.callee.type!==`MemberExpression`)return null;let i=n.callee.property.name,a=r.getText(n.callee.object);return e.has(i)?{obj:a,isNegated:!1}:t.has(i)?{obj:a,isNegated:!0}:null}function i(e){return e.alternate?e.alternate.type===`IfStatement`?1+i(e.alternate):2:1}const a={meta:{type:`suggestion`,docs:{description:`Prefer .fold() over if/else chains when working with monadic types`,recommended:!0},fixable:`code`,schema:[{type:`object`,properties:{minComplexity:{type:`integer`,minimum:1,default:2}},additionalProperties:!1}],messages:{preferFold:`Prefer .fold() over if/else when working with {{type}} types`,preferFoldTernary:`Consider using .fold() instead of ternary operator for {{type}}`}},create(e){let t=(e.options[0]||{}).minComplexity||2;function a(e,t){if(e.type===`BlockStatement`){let n=e.body;return n.length===1&&n[0].type===`ReturnStatement`?t.getText(n[0].argument):t.getText(e).slice(1,-1).trim()}else return t.getText(e)}function o(e,t){let n=t.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`);return e.replace(RegExp(`${n}\\.get\\(\\)`,`g`),`value`)}function s(t){let n=e.sourceCode;if(t.type!==`IfStatement`)return null;let i=t.test,s=t.consequent,c=t.alternate;if(!s||!c)return null;let l=r(i,n);if(!l)return null;let{obj:u,isNegated:d}=l;if(s.type===`BlockStatement`&&(s.body.length!==1||s.body[0].type!==`ReturnStatement`))return null;if(c.type===`BlockStatement`){if(c.body.length!==1||c.body[0].type!==`ReturnStatement`)return null}else if(c.type===`IfStatement`)return null;let f=a(s,n),p=a(c,n);return d?`${u}.fold(() => ${f}, (value) => ${o(p,u)})`:`${u}.fold(() => ${p}, (value) => ${o(f,u)})`}function c(t){let n=e.sourceCode;if(t.type!==`ConditionalExpression`)return null;let i=t.test,a=t.consequent,s=t.alternate,c=r(i,n);if(!c)return null;let{obj:l,isNegated:u}=c,d=n.getText(a),f=n.getText(s);return u?`${l}.fold(() => ${d}, (value) => ${o(f,l)})`:`${l}.fold(() => ${f}, (value) => ${o(d,l)})`}function l(e){if(e.type===`CallExpression`&&e.callee.type===`MemberExpression`){let t=e.callee.property.name;return[`isSome`,`isNone`,`isEmpty`,`isRight`,`isLeft`,`isSuccess`,`isFailure`].includes(t)}return!1}function u(t){let n=e.sourceCode.getText(t);if(/\.(isSome|isNone|isEmpty|isDefined)\s*\(\s*\)/.test(n))return{isMonadic:!0,type:`Option`,via:`method`};if(/\.(isLeft|isRight)\s*\(\s*\)/.test(n))return{isMonadic:!0,type:`Either`,via:`method`};if(/\.(isSuccess|isFailure)\s*\(\s*\)/.test(n))return{isMonadic:!0,type:`Result`,via:`method`};if(t.type===`BinaryExpression`){if((t.operator===`===`||t.operator===`!==`||t.operator===`==`||t.operator===`!=`)&&(t.left.type===`Literal`&&(t.left.value===null||t.left.value===void 0)||t.right.type===`Literal`&&(t.right.value===null||t.right.value===void 0)))return{isMonadic:!0,type:`Option`,via:`nullable`};if(t.operator===`==`||t.operator===`!=`||t.operator===`===`||t.operator===`!==`){let e=t.left.type===`Identifier`&&t.left.name===`undefined`,n=t.right.type===`Identifier`&&t.right.name===`undefined`;if(e||n)return{isMonadic:!0,type:`Option`,via:`nullable`}}}return{isMonadic:!1,type:``,via:``}}function d(n){let r=n.test,a=u(r);a.isMonadic&&(n.parent&&n.parent.type===`IfStatement`||i(n)>=t&&e.report({node:n,messageId:`preferFold`,data:{type:a.type},fix(e){if(!l(n.test))return null;let t=s(n);return t?e.replaceText(n,t):null}}))}return{IfStatement(e){d(e)},ConditionalExpression(t){let r=u(t.test);r.via===`nullable`&&(n(t.consequent)||n(t.alternate))||r.isMonadic&&e.report({node:t,messageId:`preferFoldTernary`,data:{type:r.type},fix(e){if(!l(t.test))return null;let n=c(t);return n?e.replaceText(t,n):null}})}}}};export{a as default};
|
|
2
2
|
//# sourceMappingURL=prefer-fold.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-fold.js","names":[],"sources":["../../src/rules/prefer-fold.ts"],"sourcesContent":["import type { Rule, SourceCode } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\n\n/** Methods on a monadic value that mean \"this is the Some/Right/Success path.\" */\nconst POSITIVE_PREDICATES: ReadonlySet<string> = new Set([\"isSome\", \"isRight\", \"isSuccess\"])\n/** Methods that mean \"this is the None/Left/Failure path.\" */\nconst NEGATIVE_PREDICATES: ReadonlySet<string> = new Set([\"isNone\", \"isEmpty\", \"isLeft\", \"isFailure\"])\n\n/**\n * If `test` is a monadic predicate call like `option.isSome()` or\n * `either.isLeft()`, returns the receiver expression's text plus whether the\n * predicate is the \"absence\" side. Returns null for anything else.\n */\nfunction extractMonadicTest(test: ASTNode, sourceCode: SourceCode): { obj: string; isNegated: boolean } | null {\n if (test.type !== \"CallExpression\" || test.callee.type !== \"MemberExpression\") return null\n const methodName = test.callee.property.name\n const obj = sourceCode.getText(test.callee.object)\n if (POSITIVE_PREDICATES.has(methodName)) return { obj, isNegated: false }\n if (NEGATIVE_PREDICATES.has(methodName)) return { obj, isNegated: true }\n return null\n}\n\n/**\n * Length of an if/else-if/else chain rooted at `node`. Counts the root as 1\n * and adds 1 per linked alternate IfStatement. Terminates on a non-If\n * alternate (the final `else { ... }`) which counts as the last branch.\n */\nfunction ifElseChainLength(node: ASTNode): number {\n if (!node.alternate) return 1\n if (node.alternate.type === \"IfStatement\") return 1 + ifElseChainLength(node.alternate as ASTNode)\n return 2 // root + the terminal else\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer .fold() over if/else chains when working with monadic types\",\n recommended: true,\n },\n fixable: \"code\",\n schema: [\n {\n type: \"object\",\n properties: {\n minComplexity: {\n type: \"integer\",\n minimum: 1,\n default: 2,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferFold: \"Prefer .fold() over if/else when working with {{type}} types\",\n preferFoldTernary: \"Consider using .fold() instead of ternary operator for {{type}}\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const minComplexity = options.minComplexity || 2\n\n function extractBodyForFold(node: ASTNode, sourceCode: SourceCode): string {\n if (node.type === \"BlockStatement\") {\n const statements = node.body\n if (statements.length === 1 && statements[0].type === \"ReturnStatement\") {\n // Extract just the return value, not the return statement\n return sourceCode.getText(statements[0].argument)\n } else {\n // For complex blocks, keep the full structure but remove outer braces\n return sourceCode.getText(node).slice(1, -1).trim()\n }\n } else {\n return sourceCode.getText(node)\n }\n }\n\n function replaceGetWithValue(body: string, monadicObj: string): string {\n // Replace monadicObj.get() with value, and monadicObj.get().chain with value.chain\n const escaped = monadicObj.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n return body.replace(new RegExp(`${escaped}\\\\.get\\\\(\\\\)`, \"g\"), \"value\")\n }\n\n function generateFoldFromIf(node: ASTNode): string | null {\n const sourceCode = context.sourceCode\n\n if (node.type !== \"IfStatement\") return null\n\n const test = node.test\n const consequent = node.consequent\n const alternate = node.alternate\n\n if (!consequent || !alternate) return null\n\n // Extract the monadic object + negation from the test predicate.\n const extracted = extractMonadicTest(test, sourceCode)\n if (!extracted) return null\n const { obj: monadicObj, isNegated } = extracted\n\n // Only handle simple cases - single return statements in blocks, no nested if statements\n if (consequent.type === \"BlockStatement\") {\n if (consequent.body.length !== 1 || consequent.body[0].type !== \"ReturnStatement\") {\n return null // Too complex, don't auto-fix\n }\n }\n\n if (alternate.type === \"BlockStatement\") {\n if (alternate.body.length !== 1 || alternate.body[0].type !== \"ReturnStatement\") {\n return null // Too complex, don't auto-fix\n }\n } else if (alternate.type === \"IfStatement\") {\n return null // Nested if/else is too complex for simple fold pattern\n }\n\n // Extract consequent and alternate bodies\n const thenBody = extractBodyForFold(consequent, sourceCode)\n const elseBody = extractBodyForFold(alternate, sourceCode)\n\n // Generate fold expression — replace .get() calls with value parameter\n if (isNegated) {\n // isNone/isLeft/isFailure: consequent is the \"none\" branch, alternate is the \"some\" branch\n const successBody = replaceGetWithValue(elseBody, monadicObj)\n return `${monadicObj}.fold(() => ${thenBody}, (value) => ${successBody})`\n } else {\n // isSome/isRight/isSuccess: consequent is the \"some\" branch, alternate is the \"none\" branch\n const successBody = replaceGetWithValue(thenBody, monadicObj)\n return `${monadicObj}.fold(() => ${elseBody}, (value) => ${successBody})`\n }\n }\n\n function generateFoldFromTernary(node: ASTNode): string | null {\n const sourceCode = context.sourceCode\n\n if (node.type !== \"ConditionalExpression\") return null\n\n const test = node.test\n const consequent = node.consequent\n const alternate = node.alternate\n\n // Extract the monadic object + negation from the test predicate.\n const extracted = extractMonadicTest(test, sourceCode)\n if (!extracted) return null\n const { obj: monadicObj, isNegated } = extracted\n\n const thenExpr = sourceCode.getText(consequent)\n const elseExpr = sourceCode.getText(alternate)\n\n // Generate fold expression — replace .get() calls with value parameter\n if (isNegated) {\n const successExpr = replaceGetWithValue(elseExpr, monadicObj)\n return `${monadicObj}.fold(() => ${thenExpr}, (value) => ${successExpr})`\n } else {\n const successExpr = replaceGetWithValue(thenExpr, monadicObj)\n return `${monadicObj}.fold(() => ${elseExpr}, (value) => ${successExpr})`\n }\n }\n\n function shouldAutoFix(node: ASTNode): boolean {\n // Only auto-fix when we detect functype method calls (indicating it's already a functype instance)\n if (node.type === \"CallExpression\" && node.callee.type === \"MemberExpression\") {\n const methodName = node.callee.property.name\n // These methods indicate the object is already a functype instance\n return [\"isSome\", \"isNone\", \"isEmpty\", \"isRight\", \"isLeft\", \"isSuccess\", \"isFailure\"].includes(methodName)\n }\n return false\n }\n\n function isMonadicCheck(node: ASTNode): { isMonadic: boolean; type: string } {\n const sourceCode = context.sourceCode\n const text = sourceCode.getText(node)\n\n // Check for common monadic type checks\n if (/\\.(isSome|isNone|isEmpty|isDefined)\\s*\\(\\s*\\)/.test(text)) {\n return { isMonadic: true, type: \"Option\" }\n }\n\n if (/\\.(isLeft|isRight)\\s*\\(\\s*\\)/.test(text)) {\n return { isMonadic: true, type: \"Either\" }\n }\n\n if (/\\.(isSuccess|isFailure)\\s*\\(\\s*\\)/.test(text)) {\n return { isMonadic: true, type: \"Result\" }\n }\n\n // Check for null/undefined checks on variables that might be Options\n if (node.type === \"BinaryExpression\") {\n if (\n (node.operator === \"===\" || node.operator === \"!==\" || node.operator === \"==\" || node.operator === \"!=\") &&\n ((node.left.type === \"Literal\" && (node.left.value === null || node.left.value === undefined)) ||\n (node.right.type === \"Literal\" && (node.right.value === null || node.right.value === undefined)))\n ) {\n return { isMonadic: true, type: \"Option\" }\n }\n\n // Check for === or == with undefined identifier\n if (node.operator === \"==\" || node.operator === \"!=\" || node.operator === \"===\" || node.operator === \"!==\") {\n const leftIsUndefined = node.left.type === \"Identifier\" && node.left.name === \"undefined\"\n const rightIsUndefined = node.right.type === \"Identifier\" && node.right.name === \"undefined\"\n\n if (leftIsUndefined || rightIsUndefined) {\n return { isMonadic: true, type: \"Option\" }\n }\n }\n }\n\n return { isMonadic: false, type: \"\" }\n }\n\n function analyzeIfStatement(node: ASTNode) {\n const test = node.test\n const monadicInfo = isMonadicCheck(test)\n\n if (!monadicInfo.isMonadic) return\n\n // Don't analyze if this is part of a larger if/else chain\n // (only analyze the outermost if statement)\n if (node.parent && node.parent.type === \"IfStatement\") return\n\n // Count the complexity (if/else if/else chain)\n if (ifElseChainLength(node) >= minComplexity) {\n context.report({\n node,\n messageId: \"preferFold\",\n data: { type: monadicInfo.type },\n fix(fixer) {\n // Only auto-fix if we can detect it's already a functype instance\n if (!shouldAutoFix(node.test)) {\n return null\n }\n const replacement = generateFoldFromIf(node)\n if (replacement) {\n return fixer.replaceText(node, replacement)\n }\n return null\n },\n })\n }\n }\n\n return {\n IfStatement(node: ASTNode) {\n analyzeIfStatement(node)\n },\n\n ConditionalExpression(node: ASTNode) {\n const monadicInfo = isMonadicCheck(node.test)\n if (monadicInfo.isMonadic) {\n context.report({\n node,\n messageId: \"preferFoldTernary\",\n data: { type: monadicInfo.type },\n fix(fixer) {\n // Only auto-fix if we can detect it's already a functype instance\n if (!shouldAutoFix(node.test)) {\n return null\n }\n const replacement = generateFoldFromTernary(node)\n if (replacement) {\n return fixer.replaceText(node, replacement)\n }\n return null\n },\n })\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prefer-fold.js","names":[],"sources":["../../src/rules/prefer-fold.ts"],"sourcesContent":["import type { Rule, SourceCode } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\n\n/** Methods on a monadic value that mean \"this is the Some/Right/Success path.\" */\nconst POSITIVE_PREDICATES: ReadonlySet<string> = new Set([\"isSome\", \"isRight\", \"isSuccess\"])\n/** Methods that mean \"this is the None/Left/Failure path.\" */\nconst NEGATIVE_PREDICATES: ReadonlySet<string> = new Set([\"isNone\", \"isEmpty\", \"isLeft\", \"isFailure\"])\n\n/**\n * A literal `null` or the `undefined` identifier. A ternary that yields one of these in a branch is\n * building an optional value (prefer-option's concern), not folding a monad — so the untyped nullable\n * heuristic must not treat `x !== undefined ? \"a\" : undefined` as a fold candidate.\n */\nfunction isNullishLiteral(node: ASTNode): boolean {\n return (node.type === \"Literal\" && node.value === null) || (node.type === \"Identifier\" && node.name === \"undefined\")\n}\n\n/**\n * If `test` is a monadic predicate call like `option.isSome()` or\n * `either.isLeft()`, returns the receiver expression's text plus whether the\n * predicate is the \"absence\" side. Returns null for anything else.\n */\nfunction extractMonadicTest(test: ASTNode, sourceCode: SourceCode): { obj: string; isNegated: boolean } | null {\n if (test.type !== \"CallExpression\" || test.callee.type !== \"MemberExpression\") return null\n const methodName = test.callee.property.name\n const obj = sourceCode.getText(test.callee.object)\n if (POSITIVE_PREDICATES.has(methodName)) return { obj, isNegated: false }\n if (NEGATIVE_PREDICATES.has(methodName)) return { obj, isNegated: true }\n return null\n}\n\n/**\n * Length of an if/else-if/else chain rooted at `node`. Counts the root as 1\n * and adds 1 per linked alternate IfStatement. Terminates on a non-If\n * alternate (the final `else { ... }`) which counts as the last branch.\n */\nfunction ifElseChainLength(node: ASTNode): number {\n if (!node.alternate) return 1\n if (node.alternate.type === \"IfStatement\") return 1 + ifElseChainLength(node.alternate as ASTNode)\n return 2 // root + the terminal else\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer .fold() over if/else chains when working with monadic types\",\n recommended: true,\n },\n fixable: \"code\",\n schema: [\n {\n type: \"object\",\n properties: {\n minComplexity: {\n type: \"integer\",\n minimum: 1,\n default: 2,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferFold: \"Prefer .fold() over if/else when working with {{type}} types\",\n preferFoldTernary: \"Consider using .fold() instead of ternary operator for {{type}}\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const minComplexity = options.minComplexity || 2\n\n function extractBodyForFold(node: ASTNode, sourceCode: SourceCode): string {\n if (node.type === \"BlockStatement\") {\n const statements = node.body\n if (statements.length === 1 && statements[0].type === \"ReturnStatement\") {\n // Extract just the return value, not the return statement\n return sourceCode.getText(statements[0].argument)\n } else {\n // For complex blocks, keep the full structure but remove outer braces\n return sourceCode.getText(node).slice(1, -1).trim()\n }\n } else {\n return sourceCode.getText(node)\n }\n }\n\n function replaceGetWithValue(body: string, monadicObj: string): string {\n // Replace monadicObj.get() with value, and monadicObj.get().chain with value.chain\n const escaped = monadicObj.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n return body.replace(new RegExp(`${escaped}\\\\.get\\\\(\\\\)`, \"g\"), \"value\")\n }\n\n function generateFoldFromIf(node: ASTNode): string | null {\n const sourceCode = context.sourceCode\n\n if (node.type !== \"IfStatement\") return null\n\n const test = node.test\n const consequent = node.consequent\n const alternate = node.alternate\n\n if (!consequent || !alternate) return null\n\n // Extract the monadic object + negation from the test predicate.\n const extracted = extractMonadicTest(test, sourceCode)\n if (!extracted) return null\n const { obj: monadicObj, isNegated } = extracted\n\n // Only handle simple cases - single return statements in blocks, no nested if statements\n if (consequent.type === \"BlockStatement\") {\n if (consequent.body.length !== 1 || consequent.body[0].type !== \"ReturnStatement\") {\n return null // Too complex, don't auto-fix\n }\n }\n\n if (alternate.type === \"BlockStatement\") {\n if (alternate.body.length !== 1 || alternate.body[0].type !== \"ReturnStatement\") {\n return null // Too complex, don't auto-fix\n }\n } else if (alternate.type === \"IfStatement\") {\n return null // Nested if/else is too complex for simple fold pattern\n }\n\n // Extract consequent and alternate bodies\n const thenBody = extractBodyForFold(consequent, sourceCode)\n const elseBody = extractBodyForFold(alternate, sourceCode)\n\n // Generate fold expression — replace .get() calls with value parameter\n if (isNegated) {\n // isNone/isLeft/isFailure: consequent is the \"none\" branch, alternate is the \"some\" branch\n const successBody = replaceGetWithValue(elseBody, monadicObj)\n return `${monadicObj}.fold(() => ${thenBody}, (value) => ${successBody})`\n } else {\n // isSome/isRight/isSuccess: consequent is the \"some\" branch, alternate is the \"none\" branch\n const successBody = replaceGetWithValue(thenBody, monadicObj)\n return `${monadicObj}.fold(() => ${elseBody}, (value) => ${successBody})`\n }\n }\n\n function generateFoldFromTernary(node: ASTNode): string | null {\n const sourceCode = context.sourceCode\n\n if (node.type !== \"ConditionalExpression\") return null\n\n const test = node.test\n const consequent = node.consequent\n const alternate = node.alternate\n\n // Extract the monadic object + negation from the test predicate.\n const extracted = extractMonadicTest(test, sourceCode)\n if (!extracted) return null\n const { obj: monadicObj, isNegated } = extracted\n\n const thenExpr = sourceCode.getText(consequent)\n const elseExpr = sourceCode.getText(alternate)\n\n // Generate fold expression — replace .get() calls with value parameter\n if (isNegated) {\n const successExpr = replaceGetWithValue(elseExpr, monadicObj)\n return `${monadicObj}.fold(() => ${thenExpr}, (value) => ${successExpr})`\n } else {\n const successExpr = replaceGetWithValue(thenExpr, monadicObj)\n return `${monadicObj}.fold(() => ${elseExpr}, (value) => ${successExpr})`\n }\n }\n\n function shouldAutoFix(node: ASTNode): boolean {\n // Only auto-fix when we detect functype method calls (indicating it's already a functype instance)\n if (node.type === \"CallExpression\" && node.callee.type === \"MemberExpression\") {\n const methodName = node.callee.property.name\n // These methods indicate the object is already a functype instance\n return [\"isSome\", \"isNone\", \"isEmpty\", \"isRight\", \"isLeft\", \"isSuccess\", \"isFailure\"].includes(methodName)\n }\n return false\n }\n\n // `via` records HOW we matched: a functype predicate call (`method`, precise) vs the untyped\n // null/undefined heuristic (`nullable`, a guess that can't tell a real Option from a plain nullable).\n function isMonadicCheck(node: ASTNode): { isMonadic: boolean; type: string; via: \"method\" | \"nullable\" | \"\" } {\n const sourceCode = context.sourceCode\n const text = sourceCode.getText(node)\n\n // Check for common monadic type checks\n if (/\\.(isSome|isNone|isEmpty|isDefined)\\s*\\(\\s*\\)/.test(text)) {\n return { isMonadic: true, type: \"Option\", via: \"method\" }\n }\n\n if (/\\.(isLeft|isRight)\\s*\\(\\s*\\)/.test(text)) {\n return { isMonadic: true, type: \"Either\", via: \"method\" }\n }\n\n if (/\\.(isSuccess|isFailure)\\s*\\(\\s*\\)/.test(text)) {\n return { isMonadic: true, type: \"Result\", via: \"method\" }\n }\n\n // Check for null/undefined checks on variables that might be Options\n if (node.type === \"BinaryExpression\") {\n if (\n (node.operator === \"===\" || node.operator === \"!==\" || node.operator === \"==\" || node.operator === \"!=\") &&\n ((node.left.type === \"Literal\" && (node.left.value === null || node.left.value === undefined)) ||\n (node.right.type === \"Literal\" && (node.right.value === null || node.right.value === undefined)))\n ) {\n return { isMonadic: true, type: \"Option\", via: \"nullable\" }\n }\n\n // Check for === or == with undefined identifier\n if (node.operator === \"==\" || node.operator === \"!=\" || node.operator === \"===\" || node.operator === \"!==\") {\n const leftIsUndefined = node.left.type === \"Identifier\" && node.left.name === \"undefined\"\n const rightIsUndefined = node.right.type === \"Identifier\" && node.right.name === \"undefined\"\n\n if (leftIsUndefined || rightIsUndefined) {\n return { isMonadic: true, type: \"Option\", via: \"nullable\" }\n }\n }\n }\n\n return { isMonadic: false, type: \"\", via: \"\" }\n }\n\n function analyzeIfStatement(node: ASTNode) {\n const test = node.test\n const monadicInfo = isMonadicCheck(test)\n\n if (!monadicInfo.isMonadic) return\n\n // Don't analyze if this is part of a larger if/else chain\n // (only analyze the outermost if statement)\n if (node.parent && node.parent.type === \"IfStatement\") return\n\n // Count the complexity (if/else if/else chain)\n if (ifElseChainLength(node) >= minComplexity) {\n context.report({\n node,\n messageId: \"preferFold\",\n data: { type: monadicInfo.type },\n fix(fixer) {\n // Only auto-fix if we can detect it's already a functype instance\n if (!shouldAutoFix(node.test)) {\n return null\n }\n const replacement = generateFoldFromIf(node)\n if (replacement) {\n return fixer.replaceText(node, replacement)\n }\n return null\n },\n })\n }\n }\n\n return {\n IfStatement(node: ASTNode) {\n analyzeIfStatement(node)\n },\n\n ConditionalExpression(node: ASTNode) {\n const monadicInfo = isMonadicCheck(node.test)\n // The untyped nullable heuristic can't distinguish a real Option from a plain nullable primitive.\n // When it fires on a ternary that yields `undefined`/`null` in a branch, that's optional value\n // construction (prefer-option's concern), not a fold — skip it. False positive this kills:\n // `x !== undefined ? \"a\" : undefined`. Predicate-call matches (`.isSome()` etc.) are unaffected.\n if (monadicInfo.via === \"nullable\" && (isNullishLiteral(node.consequent) || isNullishLiteral(node.alternate))) {\n return\n }\n if (monadicInfo.isMonadic) {\n context.report({\n node,\n messageId: \"preferFoldTernary\",\n data: { type: monadicInfo.type },\n fix(fixer) {\n // Only auto-fix if we can detect it's already a functype instance\n if (!shouldAutoFix(node.test)) {\n return null\n }\n const replacement = generateFoldFromTernary(node)\n if (replacement) {\n return fixer.replaceText(node, replacement)\n }\n return null\n },\n })\n }\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.106.
|
|
3
|
+
"version": "2.106.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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"eslint-config-prettier": "^10.1.8",
|
|
45
45
|
"ts-builds": "^3.2.1",
|
|
46
46
|
"tsdown": "^0.22.3",
|
|
47
|
-
"functype": "^1.6.
|
|
47
|
+
"functype": "^1.6.3"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public",
|