eslint-plugin-functype 2.103.1 → 2.104.1

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.
Files changed (44) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/list-rules.js +3 -3
  3. package/dist/cli/list-rules.js.map +1 -1
  4. package/dist/configs/recommended.d.ts +1 -0
  5. package/dist/configs/recommended.js +1 -1
  6. package/dist/configs/recommended.js.map +1 -1
  7. package/dist/configs/strict.d.ts +1 -0
  8. package/dist/configs/strict.js +1 -1
  9. package/dist/configs/strict.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/rules/index.d.ts +3 -1
  12. package/dist/rules/index.js +1 -1
  13. package/dist/rules/index.js.map +1 -1
  14. package/dist/rules/no-imperative-loops.js +1 -1
  15. package/dist/rules/no-imperative-loops.js.map +1 -1
  16. package/dist/rules/prefer-do-notation.js +2 -2
  17. package/dist/rules/prefer-do-notation.js.map +1 -1
  18. package/dist/rules/prefer-either.js +1 -1
  19. package/dist/rules/prefer-either.js.map +1 -1
  20. package/dist/rules/prefer-flatmap.js +1 -1
  21. package/dist/rules/prefer-flatmap.js.map +1 -1
  22. package/dist/rules/prefer-fold.js +1 -1
  23. package/dist/rules/prefer-fold.js.map +1 -1
  24. package/dist/rules/prefer-functype-map.js +1 -1
  25. package/dist/rules/prefer-functype-map.js.map +1 -1
  26. package/dist/rules/prefer-functype-set.js +1 -1
  27. package/dist/rules/prefer-functype-set.js.map +1 -1
  28. package/dist/rules/prefer-list.js +1 -1
  29. package/dist/rules/prefer-list.js.map +1 -1
  30. package/dist/rules/prefer-map.js +1 -1
  31. package/dist/rules/prefer-map.js.map +1 -1
  32. package/dist/rules/prefer-try.d.ts +7 -0
  33. package/dist/rules/prefer-try.js +2 -0
  34. package/dist/rules/prefer-try.js.map +1 -0
  35. package/dist/utils/dependency-validator.js +4 -1
  36. package/dist/utils/dependency-validator.js.map +1 -0
  37. package/dist/utils/functype-detection.d.ts +0 -3
  38. package/dist/utils/functype-detection.js +1 -1
  39. package/dist/utils/functype-detection.js.map +1 -1
  40. package/dist/utils/import-fixer.js +1 -1
  41. package/dist/utils/import-fixer.js.map +1 -1
  42. package/package.json +7 -7
  43. package/dist/dependency-validator-Cnms4306.js +0 -4
  44. package/dist/dependency-validator-Cnms4306.js.map +0 -1
@@ -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\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 from the test\n let monadicObj: string | null = null\n let isNegated = false\n\n // Handle patterns like option.isSome(), either.isLeft(), etc.\n if (test.type === \"CallExpression\" && test.callee.type === \"MemberExpression\") {\n const methodName = test.callee.property.name\n monadicObj = sourceCode.getText(test.callee.object)\n\n if (methodName === \"isSome\" || methodName === \"isRight\" || methodName === \"isSuccess\") {\n isNegated = false\n } else if (\n methodName === \"isNone\" ||\n methodName === \"isEmpty\" ||\n methodName === \"isLeft\" ||\n methodName === \"isFailure\"\n ) {\n isNegated = true\n }\n }\n\n if (!monadicObj) return null\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 from the test\n let monadicObj: string | null = null\n let isNegated = false\n\n if (test.type === \"CallExpression\" && test.callee.type === \"MemberExpression\") {\n const methodName = test.callee.property.name\n monadicObj = sourceCode.getText(test.callee.object)\n\n if (methodName === \"isSome\" || methodName === \"isRight\" || methodName === \"isSuccess\") {\n isNegated = false\n } else if (\n methodName === \"isNone\" ||\n methodName === \"isEmpty\" ||\n methodName === \"isLeft\" ||\n methodName === \"isFailure\"\n ) {\n isNegated = true\n }\n }\n\n if (!monadicObj) return null\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 let complexity = 1\n let current = node\n while (current.alternate) {\n complexity++\n if (current.alternate.type === \"IfStatement\") {\n current = current.alternate\n } else {\n break\n }\n }\n\n if (complexity >= 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":"AAIA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,KAAM,CACJ,YAAa,qEACb,YAAa,EACf,EACA,QAAS,OACT,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,cAAe,CACb,KAAM,UACN,QAAS,EACT,QAAS,CACX,CACF,EACA,qBAAsB,EACxB,CACF,EACA,SAAU,CACR,WAAY,+DACZ,kBAAmB,iEACrB,CACF,EAEA,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,CAAC,EAAA,CACT,eAAiB,EAE/C,SAAS,EAAmB,EAAe,EAAgC,CACzE,GAAI,EAAK,OAAS,iBAAkB,CAClC,IAAM,EAAa,EAAK,KAMtB,OALE,EAAW,SAAW,GAAK,EAAW,EAAE,CAAC,OAAS,kBAE7C,EAAW,QAAQ,EAAW,EAAE,CAAC,QAAQ,EAGzC,EAAW,QAAQ,CAAI,CAAC,CAAC,MAAM,EAAG,EAAE,CAAC,CAAC,KAAK,CAEtD,MACE,OAAO,EAAW,QAAQ,CAAI,CAElC,CAEA,SAAS,EAAoB,EAAc,EAA4B,CAErE,IAAM,EAAU,EAAW,QAAQ,sBAAuB,MAAM,EAChE,OAAO,EAAK,QAAY,OAAO,GAAG,EAAQ,cAAe,GAAG,EAAG,OAAO,CACxE,CAEA,SAAS,EAAmB,EAA8B,CACxD,IAAM,EAAa,EAAQ,WAE3B,GAAI,EAAK,OAAS,cAAe,OAAO,KAExC,IAAM,EAAO,EAAK,KACZ,EAAa,EAAK,WAClB,EAAY,EAAK,UAEvB,GAAI,CAAC,GAAc,CAAC,EAAW,OAAO,KAGtC,IAAI,EAA4B,KAC5B,EAAY,GAGhB,GAAI,EAAK,OAAS,kBAAoB,EAAK,OAAO,OAAS,mBAAoB,CAC7E,IAAM,EAAa,EAAK,OAAO,SAAS,KACxC,EAAa,EAAW,QAAQ,EAAK,OAAO,MAAM,EAE9C,IAAe,UAAY,IAAe,WAAa,IAAe,YACxE,EAAY,IAEZ,IAAe,UACf,IAAe,WACf,IAAe,UACf,IAAe,eAEf,EAAY,GAEhB,CAKA,GAHI,CAAC,GAGD,EAAW,OAAS,mBAClB,EAAW,KAAK,SAAW,GAAK,EAAW,KAAK,EAAE,CAAC,OAAS,mBAC9D,OAAO,KAIX,GAAI,EAAU,OAAS,qBACjB,EAAU,KAAK,SAAW,GAAK,EAAU,KAAK,EAAE,CAAC,OAAS,kBAC5D,OAAO,IAAA,MAEJ,GAAI,EAAU,OAAS,cAC5B,OAAO,KAIT,IAAM,EAAW,EAAmB,EAAY,CAAU,EACpD,EAAW,EAAmB,EAAW,CAAU,EAGzD,GAAI,EAAW,CAEb,IAAM,EAAc,EAAoB,EAAU,CAAU,EAC5D,MAAO,GAAG,EAAW,cAAc,EAAS,eAAe,EAAY,EACzE,KAAO,CAEL,IAAM,EAAc,EAAoB,EAAU,CAAU,EAC5D,MAAO,GAAG,EAAW,cAAc,EAAS,eAAe,EAAY,EACzE,CACF,CAEA,SAAS,EAAwB,EAA8B,CAC7D,IAAM,EAAa,EAAQ,WAE3B,GAAI,EAAK,OAAS,wBAAyB,OAAO,KAElD,IAAM,EAAO,EAAK,KACZ,EAAa,EAAK,WAClB,EAAY,EAAK,UAGnB,EAA4B,KAC5B,EAAY,GAEhB,GAAI,EAAK,OAAS,kBAAoB,EAAK,OAAO,OAAS,mBAAoB,CAC7E,IAAM,EAAa,EAAK,OAAO,SAAS,KACxC,EAAa,EAAW,QAAQ,EAAK,OAAO,MAAM,EAE9C,IAAe,UAAY,IAAe,WAAa,IAAe,YACxE,EAAY,IAEZ,IAAe,UACf,IAAe,WACf,IAAe,UACf,IAAe,eAEf,EAAY,GAEhB,CAEA,GAAI,CAAC,EAAY,OAAO,KAExB,IAAM,EAAW,EAAW,QAAQ,CAAU,EACxC,EAAW,EAAW,QAAQ,CAAS,EAG7C,GAAI,EAAW,CACb,IAAM,EAAc,EAAoB,EAAU,CAAU,EAC5D,MAAO,GAAG,EAAW,cAAc,EAAS,eAAe,EAAY,EACzE,KAAO,CACL,IAAM,EAAc,EAAoB,EAAU,CAAU,EAC5D,MAAO,GAAG,EAAW,cAAc,EAAS,eAAe,EAAY,EACzE,CACF,CAEA,SAAS,EAAc,EAAwB,CAE7C,GAAI,EAAK,OAAS,kBAAoB,EAAK,OAAO,OAAS,mBAAoB,CAC7E,IAAM,EAAa,EAAK,OAAO,SAAS,KAExC,MAAO,CAAC,SAAU,SAAU,UAAW,UAAW,SAAU,YAAa,WAAW,CAAC,CAAC,SAAS,CAAU,CAC3G,CACA,MAAO,EACT,CAEA,SAAS,EAAe,EAAqD,CAE3E,IAAM,EADa,EAAQ,WACH,QAAQ,CAAI,EAGpC,GAAI,gDAAgD,KAAK,CAAI,EAC3D,MAAO,CAAE,UAAW,GAAM,KAAM,QAAS,EAG3C,GAAI,+BAA+B,KAAK,CAAI,EAC1C,MAAO,CAAE,UAAW,GAAM,KAAM,QAAS,EAG3C,GAAI,oCAAoC,KAAK,CAAI,EAC/C,MAAO,CAAE,UAAW,GAAM,KAAM,QAAS,EAI3C,GAAI,EAAK,OAAS,mBAAoB,CACpC,IACG,EAAK,WAAa,OAAS,EAAK,WAAa,OAAS,EAAK,WAAa,MAAQ,EAAK,WAAa,QACjG,EAAK,KAAK,OAAS,YAAc,EAAK,KAAK,QAAU,MAAQ,EAAK,KAAK,QAAU,IAAA,KAChF,EAAK,MAAM,OAAS,YAAc,EAAK,MAAM,QAAU,MAAQ,EAAK,MAAM,QAAU,IAAA,KAEvF,MAAO,CAAE,UAAW,GAAM,KAAM,QAAS,EAI3C,GAAI,EAAK,WAAa,MAAQ,EAAK,WAAa,MAAQ,EAAK,WAAa,OAAS,EAAK,WAAa,MAAO,CAC1G,IAAM,EAAkB,EAAK,KAAK,OAAS,cAAgB,EAAK,KAAK,OAAS,YACxE,EAAmB,EAAK,MAAM,OAAS,cAAgB,EAAK,MAAM,OAAS,YAEjF,GAAI,GAAmB,EACrB,MAAO,CAAE,UAAW,GAAM,KAAM,QAAS,CAE7C,CACF,CAEA,MAAO,CAAE,UAAW,GAAO,KAAM,EAAG,CACtC,CAEA,SAAS,EAAmB,EAAe,CACzC,IAAM,EAAO,EAAK,KACZ,EAAc,EAAe,CAAI,EAMvC,GAJI,CAAC,EAAY,WAIb,EAAK,QAAU,EAAK,OAAO,OAAS,cAAe,OAGvD,IAAI,EAAa,EACb,EAAU,EACd,KAAO,EAAQ,YACb,IACI,EAAQ,UAAU,OAAS,gBAC7B,EAAU,EAAQ,UAMlB,GAAc,GAChB,EAAQ,OAAO,CACb,OACA,UAAW,aACX,KAAM,CAAE,KAAM,EAAY,IAAK,EAC/B,IAAI,EAAO,CAET,GAAI,CAAC,EAAc,EAAK,IAAI,EAC1B,OAAO,KAET,IAAM,EAAc,EAAmB,CAAI,EAI3C,OAHI,EACK,EAAM,YAAY,EAAM,CAAW,EAErC,IACT,CACF,CAAC,CAEL,CAEA,MAAO,CACL,YAAY,EAAe,CACzB,EAAmB,CAAI,CACzB,EAEA,sBAAsB,EAAe,CACnC,IAAM,EAAc,EAAe,EAAK,IAAI,EACxC,EAAY,WACd,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,KAAM,EAAY,IAAK,EAC/B,IAAI,EAAO,CAET,GAAI,CAAC,EAAc,EAAK,IAAI,EAC1B,OAAO,KAET,IAAM,EAAc,EAAwB,CAAI,EAIhD,OAHI,EACK,EAAM,YAAY,EAAM,CAAW,EAErC,IACT,CACF,CAAC,CAEL,CACF,CACF,CACF"}
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,2 +1,2 @@
1
- import{getFunctypeImportsLegacy as e,isAlreadyUsingFunctype as t}from"../utils/functype-detection.js";import{createImportFixer as n,hasFunctypeSymbol as r}from"../utils/import-fixer.js";const i={meta:{type:`suggestion`,hasSuggestions:!0,docs:{description:`Prefer functype Map<K, V> over native Map for immutable key-value collections`,recommended:!0},schema:[{type:`object`,properties:{allowInTests:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferFunctypeMap:`Prefer functype Map<{{keyType}}, {{valueType}}> over native Map`,preferFunctypeMapLiteral:`Prefer Map.of(...) or Map.empty() over new Map()`,suggestMapEmpty:`Replace with Map.empty()`,suggestMapOf:`Replace with Map.of(...)`,suggestMapFrom:`Replace with Map(...)`,suggestAddImport:`Add {{symbol}} import from functype`}},create(i){let a=(i.options[0]||{}).allowInTests!==!1;function o(){let e=i.filename;return/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(e)||e.includes(`__tests__`)||e.includes(`/test/`)||e.includes(`/tests/`)}let s=e(i);function c(){return r(i.sourceCode,`Map`)}return{NewExpression(e){if(a&&o()||!e.callee||e.callee.type!==`Identifier`||e.callee.name!==`Map`||c()||t(e,s))return;let r=i.sourceCode,l=e.arguments,u=[];if(l.length===0)u.push({messageId:`suggestMapEmpty`,fix(t){return t.replaceText(e,`Map.empty()`)}});else if(l.length===1&&l[0].type===`ArrayExpression`){let t=l[0].elements.map(e=>r.getText(e)).join(`, `);u.push({messageId:`suggestMapOf`,fix(n){return n.replaceText(e,`Map.of(${t})`)}})}else if(l.length===1){let t=r.getText(l[0]);u.push({messageId:`suggestMapFrom`,fix(n){return n.replaceText(e,`Map(${t})`)}})}else u.push({messageId:`suggestMapEmpty`,fix(t){return t.replaceText(e,`Map.empty()`)}});c()||u.push({messageId:`suggestAddImport`,data:{symbol:`Map`},fix:n(r,`Map`)}),i.report({node:e,messageId:`preferFunctypeMapLiteral`,suggest:u})},TSTypeReference(e){if(a&&o()||!e.typeName)return;let t=i.sourceCode;if((e.typeName.type===`Identifier`?e.typeName.name:t.getText(e.typeName))!==`Map`||c())return;let r=`K`,s=`V`,l=e.typeArguments?.params??e.typeParameters?.params;l&&l.length>=2?(r=t.getText(l[0]),s=t.getText(l[1])):l&&l.length===1&&(r=t.getText(l[0]));let u=[{messageId:`suggestAddImport`,data:{symbol:`Map`},fix:n(t,`Map`)}];i.report({node:e,messageId:`preferFunctypeMap`,data:{keyType:r,valueType:s},suggest:u})}}}};export{i as default};
1
+ import{getFunctypeImportsLegacy as e,isAlreadyUsingFunctype as t}from"../utils/functype-detection.js";import{createImportFixer as n,hasFunctypeSymbol as r}from"../utils/import-fixer.js";const i={meta:{type:`suggestion`,hasSuggestions:!0,docs:{description:`Prefer functype Map<K, V> over native Map for immutable key-value collections`,recommended:!0},schema:[{type:`object`,properties:{allowInTests:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferFunctypeMap:`Prefer functype Map<{{keyType}}, {{valueType}}> over native Map`,preferFunctypeMapLiteral:`Prefer Map.of(...) or Map.empty() over new Map()`,suggestMapEmpty:`Replace with Map.empty()`,suggestMapOf:`Replace with Map.of(...)`,suggestMapFrom:`Replace with Map(...)`,suggestAddImport:`Add {{symbol}} import from functype`}},create(i){let a=(i.options[0]||{}).allowInTests!==!1;function o(){let e=i.filename;return/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(e)||e.includes(`__tests__`)||e.includes(`/test/`)||e.includes(`/tests/`)}let s=e(i);function c(){return r(i.sourceCode,`Map`)}return{NewExpression(e){if(a&&o()||!e.callee||e.callee.type!==`Identifier`||e.callee.name!==`Map`||c()||t(e,s))return;let r=i.sourceCode,l=e.arguments,u=[];if(l.length===0)u.push({messageId:`suggestMapEmpty`,fix(t){return t.replaceText(e,`Map.empty()`)}});else if(l.length===1&&l[0].type===`ArrayExpression`){let t=l[0].elements.map(e=>r.getText(e)).join(`, `);u.push({messageId:`suggestMapOf`,fix(n){return n.replaceText(e,`Map.of(${t})`)}})}else if(l.length===1){let t=r.getText(l[0]);u.push({messageId:`suggestMapFrom`,fix(n){return n.replaceText(e,`Map(${t})`)}})}else u.push({messageId:`suggestMapEmpty`,fix(t){return t.replaceText(e,`Map.empty()`)}});c()||u.push({messageId:`suggestAddImport`,data:{symbol:`Map`},fix:n(r,`Map`)}),i.report({node:e,messageId:`preferFunctypeMapLiteral`,suggest:u})},TSTypeReference(e){if(a&&o()||!e.typeName)return;let t=i.sourceCode;if((e.typeName.type===`Identifier`?e.typeName.name:t.getText(e.typeName))!==`Map`||c())return;let r=e.typeArguments?.params??e.typeParameters?.params,s=r?.[0]?t.getText(r[0]):`K`,l=r&&r.length>=2?t.getText(r[1]):`V`,u=[{messageId:`suggestAddImport`,data:{symbol:`Map`},fix:n(t,`Map`)}];i.report({node:e,messageId:`preferFunctypeMap`,data:{keyType:s,valueType:l},suggest:u})}}}};export{i as default};
2
2
  //# sourceMappingURL=prefer-functype-map.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-functype-map.js","names":[],"sources":["../../src/rules/prefer-functype-map.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImportsLegacy, isAlreadyUsingFunctype } from \"../utils/functype-detection\"\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 functype Map<K, V> over native Map for immutable key-value collections\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowInTests: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferFunctypeMap: \"Prefer functype Map<{{keyType}}, {{valueType}}> over native Map\",\n preferFunctypeMapLiteral: \"Prefer Map.of(...) or Map.empty() over new Map()\",\n suggestMapEmpty: \"Replace with Map.empty()\",\n suggestMapOf: \"Replace with Map.of(...)\",\n suggestMapFrom: \"Replace with Map(...)\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const allowInTests = options.allowInTests !== 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 const functypeImports = getFunctypeImportsLegacy(context)\n\n function isMapImportedFromFunctype(): boolean {\n return hasFunctypeSymbol(context.sourceCode, \"Map\")\n }\n\n return {\n NewExpression(node: ASTNode) {\n if (allowInTests && isInTestFile()) return\n\n // Only flag `new Map(...)` calls\n if (!node.callee || node.callee.type !== \"Identifier\" || node.callee.name !== \"Map\") return\n\n // Skip if Map is already imported from functype\n if (isMapImportedFromFunctype()) return\n\n // Skip if already using functype\n if (isAlreadyUsingFunctype(node, functypeImports)) return\n\n const sourceCode = context.sourceCode\n const args = node.arguments as ASTNode[]\n\n const suggestions: Rule.SuggestionReportDescriptor[] = []\n\n if (args.length === 0) {\n // new Map() → Map.empty()\n suggestions.push({\n messageId: \"suggestMapEmpty\",\n fix(fixer) {\n return fixer.replaceText(node, \"Map.empty()\")\n },\n })\n } else if (args.length === 1 && args[0].type === \"ArrayExpression\") {\n // new Map([[\"a\", 1], [\"b\", 2]]) → Map.of([\"a\", 1], [\"b\", 2])\n const arrayArg = args[0] as ASTNode\n const elements = arrayArg.elements as ASTNode[]\n const tupleTexts = elements.map((el: ASTNode) => sourceCode.getText(el))\n const mapOfArgs = tupleTexts.join(\", \")\n suggestions.push({\n messageId: \"suggestMapOf\",\n fix(fixer) {\n return fixer.replaceText(node, `Map.of(${mapOfArgs})`)\n },\n })\n } else if (args.length === 1) {\n // new Map(someVar) → Map(someVar)\n const argText = sourceCode.getText(args[0])\n suggestions.push({\n messageId: \"suggestMapFrom\",\n fix(fixer) {\n return fixer.replaceText(node, `Map(${argText})`)\n },\n })\n } else {\n // Generic fallback for any other args\n suggestions.push({\n messageId: \"suggestMapEmpty\",\n fix(fixer) {\n return fixer.replaceText(node, \"Map.empty()\")\n },\n })\n }\n\n if (!isMapImportedFromFunctype()) {\n suggestions.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Map\" },\n fix: createImportFixer(sourceCode, \"Map\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferFunctypeMapLiteral\",\n suggest: suggestions,\n })\n },\n\n TSTypeReference(node: ASTNode) {\n if (allowInTests && isInTestFile()) return\n\n if (!node.typeName) return\n\n const sourceCode = context.sourceCode\n const typeName = node.typeName.type === \"Identifier\" ? node.typeName.name : sourceCode.getText(node.typeName)\n\n if (typeName !== \"Map\") return\n\n // Skip if Map is already imported from functype\n if (isMapImportedFromFunctype()) return\n\n // Extract key/value type params if present (typeArguments for newer TS-ESLint, typeParameters for older)\n let keyType = \"K\"\n let valueType = \"V\"\n const typeParams = node.typeArguments?.params ?? node.typeParameters?.params\n if (typeParams && typeParams.length >= 2) {\n keyType = sourceCode.getText(typeParams[0])\n valueType = sourceCode.getText(typeParams[1])\n } else if (typeParams && typeParams.length === 1) {\n keyType = sourceCode.getText(typeParams[0])\n }\n\n const suggestions: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestAddImport\",\n data: { symbol: \"Map\" },\n fix: createImportFixer(sourceCode, \"Map\"),\n },\n ]\n\n context.report({\n node,\n messageId: \"preferFunctypeMap\",\n data: { keyType, valueType },\n suggest: suggestions,\n })\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
1
+ {"version":3,"file":"prefer-functype-map.js","names":[],"sources":["../../src/rules/prefer-functype-map.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImportsLegacy, isAlreadyUsingFunctype } from \"../utils/functype-detection\"\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 functype Map<K, V> over native Map for immutable key-value collections\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowInTests: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferFunctypeMap: \"Prefer functype Map<{{keyType}}, {{valueType}}> over native Map\",\n preferFunctypeMapLiteral: \"Prefer Map.of(...) or Map.empty() over new Map()\",\n suggestMapEmpty: \"Replace with Map.empty()\",\n suggestMapOf: \"Replace with Map.of(...)\",\n suggestMapFrom: \"Replace with Map(...)\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const allowInTests = options.allowInTests !== 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 const functypeImports = getFunctypeImportsLegacy(context)\n\n function isMapImportedFromFunctype(): boolean {\n return hasFunctypeSymbol(context.sourceCode, \"Map\")\n }\n\n return {\n NewExpression(node: ASTNode) {\n if (allowInTests && isInTestFile()) return\n\n // Only flag `new Map(...)` calls\n if (!node.callee || node.callee.type !== \"Identifier\" || node.callee.name !== \"Map\") return\n\n // Skip if Map is already imported from functype\n if (isMapImportedFromFunctype()) return\n\n // Skip if already using functype\n if (isAlreadyUsingFunctype(node, functypeImports)) return\n\n const sourceCode = context.sourceCode\n const args = node.arguments as ASTNode[]\n\n const suggestions: Rule.SuggestionReportDescriptor[] = []\n\n if (args.length === 0) {\n // new Map() → Map.empty()\n suggestions.push({\n messageId: \"suggestMapEmpty\",\n fix(fixer) {\n return fixer.replaceText(node, \"Map.empty()\")\n },\n })\n } else if (args.length === 1 && args[0].type === \"ArrayExpression\") {\n // new Map([[\"a\", 1], [\"b\", 2]]) → Map.of([\"a\", 1], [\"b\", 2])\n const arrayArg = args[0] as ASTNode\n const elements = arrayArg.elements as ASTNode[]\n const tupleTexts = elements.map((el: ASTNode) => sourceCode.getText(el))\n const mapOfArgs = tupleTexts.join(\", \")\n suggestions.push({\n messageId: \"suggestMapOf\",\n fix(fixer) {\n return fixer.replaceText(node, `Map.of(${mapOfArgs})`)\n },\n })\n } else if (args.length === 1) {\n // new Map(someVar) → Map(someVar)\n const argText = sourceCode.getText(args[0])\n suggestions.push({\n messageId: \"suggestMapFrom\",\n fix(fixer) {\n return fixer.replaceText(node, `Map(${argText})`)\n },\n })\n } else {\n // Generic fallback for any other args\n suggestions.push({\n messageId: \"suggestMapEmpty\",\n fix(fixer) {\n return fixer.replaceText(node, \"Map.empty()\")\n },\n })\n }\n\n if (!isMapImportedFromFunctype()) {\n suggestions.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Map\" },\n fix: createImportFixer(sourceCode, \"Map\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferFunctypeMapLiteral\",\n suggest: suggestions,\n })\n },\n\n TSTypeReference(node: ASTNode) {\n if (allowInTests && isInTestFile()) return\n\n if (!node.typeName) return\n\n const sourceCode = context.sourceCode\n const typeName = node.typeName.type === \"Identifier\" ? node.typeName.name : sourceCode.getText(node.typeName)\n\n if (typeName !== \"Map\") return\n\n // Skip if Map is already imported from functype\n if (isMapImportedFromFunctype()) return\n\n // Extract key/value type params if present (typeArguments for newer TS-ESLint, typeParameters for older)\n const typeParams = node.typeArguments?.params ?? node.typeParameters?.params\n const keyType = typeParams?.[0] ? sourceCode.getText(typeParams[0]) : \"K\"\n const valueType = typeParams && typeParams.length >= 2 ? sourceCode.getText(typeParams[1]) : \"V\"\n\n const suggestions: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestAddImport\",\n data: { symbol: \"Map\" },\n fix: createImportFixer(sourceCode, \"Map\"),\n },\n ]\n\n context.report({\n node,\n messageId: \"preferFunctypeMap\",\n data: { keyType, valueType },\n suggest: suggestions,\n })\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{getFunctypeImportsLegacy as e,isAlreadyUsingFunctype as t}from"../utils/functype-detection.js";import{createImportFixer as n,hasFunctypeSymbol as r}from"../utils/import-fixer.js";const i={meta:{type:`suggestion`,hasSuggestions:!0,docs:{description:`Prefer functype Set<T> over native Set for immutable collections`,recommended:!0},schema:[{type:`object`,properties:{allowInTests:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferFunctypeSet:`Prefer functype Set<{{type}}> over native Set`,preferFunctypeSetLiteral:`Prefer Set.of(...) or Set.empty() over new Set()`,suggestSetEmpty:`Replace with Set.empty()`,suggestSetOf:`Replace with Set.of(...)`,suggestSetFrom:`Replace with Set(...)`,suggestAddImport:`Add {{symbol}} import from functype`}},create(i){let a=(i.options[0]||{}).allowInTests!==!1;function o(){let e=i.filename;return/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(e)||e.includes(`__tests__`)||e.includes(`/test/`)||e.includes(`/tests/`)}let s=e(i);function c(){return r(i.sourceCode,`Set`)}return{NewExpression(e){if(a&&o()||!e.callee||e.callee.type!==`Identifier`||e.callee.name!==`Set`||c()||t(e,s))return;let r=i.sourceCode,l=e.arguments||[],u=[];if(l.length===0)u.push({messageId:`suggestSetEmpty`,fix(t){return t.replaceText(e,`Set.empty()`)}});else if(l.length===1&&l[0].type===`ArrayExpression`){let t=(l[0].elements||[]).map(e=>r.getText(e)).join(`, `);u.push({messageId:`suggestSetOf`,fix(n){return n.replaceText(e,`Set.of(${t})`)}})}else if(l.length===1){let t=r.getText(l[0]);u.push({messageId:`suggestSetFrom`,fix(n){return n.replaceText(e,`Set(${t})`)}})}else u.push({messageId:`suggestSetEmpty`,fix(t){return t.replaceText(e,`Set.empty()`)}});c()||u.push({messageId:`suggestAddImport`,data:{symbol:`Set`},fix:n(r,`Set`)}),i.report({node:e,messageId:`preferFunctypeSetLiteral`,suggest:u})},TSTypeReference(e){if(a&&o()||!e.typeName)return;let t=i.sourceCode;if((e.typeName.type===`Identifier`?e.typeName.name:t.getText(e.typeName))!==`Set`||c())return;let r=`T`;e.typeParameters?.params?.[0]?r=t.getText(e.typeParameters.params[0]):e.typeArguments?.params?.[0]&&(r=t.getText(e.typeArguments.params[0]));let s=[{messageId:`suggestAddImport`,data:{symbol:`Set`},fix:n(t,`Set`)}];i.report({node:e,messageId:`preferFunctypeSet`,data:{type:r},suggest:s})}}}};export{i as default};
1
+ import{getFunctypeImportsLegacy as e,isAlreadyUsingFunctype as t}from"../utils/functype-detection.js";import{createImportFixer as n,hasFunctypeSymbol as r}from"../utils/import-fixer.js";const i={meta:{type:`suggestion`,hasSuggestions:!0,docs:{description:`Prefer functype Set<T> over native Set for immutable collections`,recommended:!0},schema:[{type:`object`,properties:{allowInTests:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferFunctypeSet:`Prefer functype Set<{{type}}> over native Set`,preferFunctypeSetLiteral:`Prefer Set.of(...) or Set.empty() over new Set()`,suggestSetEmpty:`Replace with Set.empty()`,suggestSetOf:`Replace with Set.of(...)`,suggestSetFrom:`Replace with Set(...)`,suggestAddImport:`Add {{symbol}} import from functype`}},create(i){let a=(i.options[0]||{}).allowInTests!==!1;function o(){let e=i.filename;return/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(e)||e.includes(`__tests__`)||e.includes(`/test/`)||e.includes(`/tests/`)}let s=e(i);function c(){return r(i.sourceCode,`Set`)}return{NewExpression(e){if(a&&o()||!e.callee||e.callee.type!==`Identifier`||e.callee.name!==`Set`||c()||t(e,s))return;let r=i.sourceCode,l=e.arguments||[],u=[];if(l.length===0)u.push({messageId:`suggestSetEmpty`,fix(t){return t.replaceText(e,`Set.empty()`)}});else if(l.length===1&&l[0].type===`ArrayExpression`){let t=(l[0].elements||[]).map(e=>r.getText(e)).join(`, `);u.push({messageId:`suggestSetOf`,fix(n){return n.replaceText(e,`Set.of(${t})`)}})}else if(l.length===1){let t=r.getText(l[0]);u.push({messageId:`suggestSetFrom`,fix(n){return n.replaceText(e,`Set(${t})`)}})}else u.push({messageId:`suggestSetEmpty`,fix(t){return t.replaceText(e,`Set.empty()`)}});c()||u.push({messageId:`suggestAddImport`,data:{symbol:`Set`},fix:n(r,`Set`)}),i.report({node:e,messageId:`preferFunctypeSetLiteral`,suggest:u})},TSTypeReference(e){if(a&&o()||!e.typeName)return;let t=i.sourceCode;if((e.typeName.type===`Identifier`?e.typeName.name:t.getText(e.typeName))!==`Set`||c())return;let r=e.typeParameters?.params?.[0]??e.typeArguments?.params?.[0],s=r?t.getText(r):`T`,l=[{messageId:`suggestAddImport`,data:{symbol:`Set`},fix:n(t,`Set`)}];i.report({node:e,messageId:`preferFunctypeSet`,data:{type:s},suggest:l})}}}};export{i as default};
2
2
  //# sourceMappingURL=prefer-functype-set.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-functype-set.js","names":[],"sources":["../../src/rules/prefer-functype-set.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImportsLegacy, isAlreadyUsingFunctype } from \"../utils/functype-detection\"\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 functype Set<T> over native Set for immutable collections\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowInTests: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferFunctypeSet: \"Prefer functype Set<{{type}}> over native Set\",\n preferFunctypeSetLiteral: \"Prefer Set.of(...) or Set.empty() over new Set()\",\n suggestSetEmpty: \"Replace with Set.empty()\",\n suggestSetOf: \"Replace with Set.of(...)\",\n suggestSetFrom: \"Replace with Set(...)\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const allowInTests = options.allowInTests !== 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 const functypeImports = getFunctypeImportsLegacy(context)\n\n function isSetImportedFromFunctype() {\n return hasFunctypeSymbol(context.sourceCode, \"Set\")\n }\n\n return {\n NewExpression(node: ASTNode) {\n if (allowInTests && isInTestFile()) return\n\n // Only handle `new Set(...)` calls\n if (!node.callee || node.callee.type !== \"Identifier\" || node.callee.name !== \"Set\") return\n\n // Skip if Set is already imported from functype\n if (isSetImportedFromFunctype()) return\n\n // Skip if already in a functype context\n if (isAlreadyUsingFunctype(node, functypeImports)) return\n\n const sourceCode = context.sourceCode\n const args = node.arguments || []\n const suggestions: Rule.SuggestionReportDescriptor[] = []\n\n if (args.length === 0) {\n // new Set() → Set.empty()\n suggestions.push({\n messageId: \"suggestSetEmpty\",\n fix(fixer) {\n return fixer.replaceText(node, \"Set.empty()\")\n },\n })\n } else if (args.length === 1 && args[0].type === \"ArrayExpression\") {\n // new Set([\"a\", \"b\", \"c\"]) → Set.of(\"a\", \"b\", \"c\")\n const arrayNode = args[0]\n const elements = arrayNode.elements || []\n const elementTexts = elements.map((el: ASTNode) => sourceCode.getText(el))\n const argsText = elementTexts.join(\", \")\n suggestions.push({\n messageId: \"suggestSetOf\",\n fix(fixer) {\n return fixer.replaceText(node, `Set.of(${argsText})`)\n },\n })\n } else if (args.length === 1) {\n // new Set(someVar) → Set(someVar)\n const argText = sourceCode.getText(args[0])\n suggestions.push({\n messageId: \"suggestSetFrom\",\n fix(fixer) {\n return fixer.replaceText(node, `Set(${argText})`)\n },\n })\n } else {\n // Fallback for unexpected cases\n suggestions.push({\n messageId: \"suggestSetEmpty\",\n fix(fixer) {\n return fixer.replaceText(node, \"Set.empty()\")\n },\n })\n }\n\n if (!isSetImportedFromFunctype()) {\n suggestions.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Set\" },\n fix: createImportFixer(sourceCode, \"Set\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferFunctypeSetLiteral\",\n suggest: suggestions,\n })\n },\n\n TSTypeReference(node: ASTNode) {\n if (allowInTests && isInTestFile()) return\n\n if (!node.typeName) return\n\n const sourceCode = context.sourceCode\n const typeName = node.typeName.type === \"Identifier\" ? node.typeName.name : sourceCode.getText(node.typeName)\n\n if (typeName !== \"Set\") return\n\n // Skip if Set is already imported from functype\n if (isSetImportedFromFunctype()) return\n\n // Extract type parameter if present\n let typeParam = \"T\"\n if (node.typeParameters?.params?.[0]) {\n typeParam = sourceCode.getText(node.typeParameters.params[0])\n } else if (node.typeArguments?.params?.[0]) {\n typeParam = sourceCode.getText(node.typeArguments.params[0])\n }\n\n const suggestions: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestAddImport\",\n data: { symbol: \"Set\" },\n fix: createImportFixer(sourceCode, \"Set\"),\n },\n ]\n\n context.report({\n node,\n messageId: \"preferFunctypeSet\",\n data: { type: typeParam },\n suggest: suggestions,\n })\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
1
+ {"version":3,"file":"prefer-functype-set.js","names":[],"sources":["../../src/rules/prefer-functype-set.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImportsLegacy, isAlreadyUsingFunctype } from \"../utils/functype-detection\"\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 functype Set<T> over native Set for immutable collections\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowInTests: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferFunctypeSet: \"Prefer functype Set<{{type}}> over native Set\",\n preferFunctypeSetLiteral: \"Prefer Set.of(...) or Set.empty() over new Set()\",\n suggestSetEmpty: \"Replace with Set.empty()\",\n suggestSetOf: \"Replace with Set.of(...)\",\n suggestSetFrom: \"Replace with Set(...)\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const allowInTests = options.allowInTests !== 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 const functypeImports = getFunctypeImportsLegacy(context)\n\n function isSetImportedFromFunctype() {\n return hasFunctypeSymbol(context.sourceCode, \"Set\")\n }\n\n return {\n NewExpression(node: ASTNode) {\n if (allowInTests && isInTestFile()) return\n\n // Only handle `new Set(...)` calls\n if (!node.callee || node.callee.type !== \"Identifier\" || node.callee.name !== \"Set\") return\n\n // Skip if Set is already imported from functype\n if (isSetImportedFromFunctype()) return\n\n // Skip if already in a functype context\n if (isAlreadyUsingFunctype(node, functypeImports)) return\n\n const sourceCode = context.sourceCode\n const args = node.arguments || []\n const suggestions: Rule.SuggestionReportDescriptor[] = []\n\n if (args.length === 0) {\n // new Set() → Set.empty()\n suggestions.push({\n messageId: \"suggestSetEmpty\",\n fix(fixer) {\n return fixer.replaceText(node, \"Set.empty()\")\n },\n })\n } else if (args.length === 1 && args[0].type === \"ArrayExpression\") {\n // new Set([\"a\", \"b\", \"c\"]) → Set.of(\"a\", \"b\", \"c\")\n const arrayNode = args[0]\n const elements = arrayNode.elements || []\n const elementTexts = elements.map((el: ASTNode) => sourceCode.getText(el))\n const argsText = elementTexts.join(\", \")\n suggestions.push({\n messageId: \"suggestSetOf\",\n fix(fixer) {\n return fixer.replaceText(node, `Set.of(${argsText})`)\n },\n })\n } else if (args.length === 1) {\n // new Set(someVar) → Set(someVar)\n const argText = sourceCode.getText(args[0])\n suggestions.push({\n messageId: \"suggestSetFrom\",\n fix(fixer) {\n return fixer.replaceText(node, `Set(${argText})`)\n },\n })\n } else {\n // Fallback for unexpected cases\n suggestions.push({\n messageId: \"suggestSetEmpty\",\n fix(fixer) {\n return fixer.replaceText(node, \"Set.empty()\")\n },\n })\n }\n\n if (!isSetImportedFromFunctype()) {\n suggestions.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Set\" },\n fix: createImportFixer(sourceCode, \"Set\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferFunctypeSetLiteral\",\n suggest: suggestions,\n })\n },\n\n TSTypeReference(node: ASTNode) {\n if (allowInTests && isInTestFile()) return\n\n if (!node.typeName) return\n\n const sourceCode = context.sourceCode\n const typeName = node.typeName.type === \"Identifier\" ? node.typeName.name : sourceCode.getText(node.typeName)\n\n if (typeName !== \"Set\") return\n\n // Skip if Set is already imported from functype\n if (isSetImportedFromFunctype()) return\n\n // Extract type parameter if present (typeArguments for newer TS-ESLint, typeParameters for older)\n const typeParamNode = node.typeParameters?.params?.[0] ?? node.typeArguments?.params?.[0]\n const typeParam = typeParamNode ? sourceCode.getText(typeParamNode) : \"T\"\n\n const suggestions: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestAddImport\",\n data: { symbol: \"Set\" },\n fix: createImportFixer(sourceCode, \"Set\"),\n },\n ]\n\n context.report({\n node,\n messageId: \"preferFunctypeSet\",\n data: { type: typeParam },\n suggest: suggestions,\n })\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"0LAMA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,mEACb,YAAa,EACf,EACA,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,aAAc,CACZ,KAAM,UACN,QAAS,EACX,CACF,EACA,qBAAsB,EACxB,CACF,EACA,SAAU,CACR,kBAAmB,gDACnB,yBAA0B,mDAC1B,gBAAiB,2BACjB,aAAc,2BACd,eAAgB,wBAChB,iBAAkB,qCACpB,CACF,EAEA,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,CAAC,EAAA,CACV,eAAiB,GAE9C,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,IAAM,EAAkB,EAAyB,CAAO,EAExD,SAAS,GAA4B,CACnC,OAAO,EAAkB,EAAQ,WAAY,KAAK,CACpD,CAEA,MAAO,CACL,cAAc,EAAe,CAU3B,GATI,GAAgB,EAAa,GAG7B,CAAC,EAAK,QAAU,EAAK,OAAO,OAAS,cAAgB,EAAK,OAAO,OAAS,OAG1E,EAA0B,GAG1B,EAAuB,EAAM,CAAe,EAAG,OAEnD,IAAM,EAAa,EAAQ,WACrB,EAAO,EAAK,WAAa,CAAC,EAC1B,EAAiD,CAAC,EAExD,GAAI,EAAK,SAAW,EAElB,EAAY,KAAK,CACf,UAAW,kBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,aAAa,CAC9C,CACF,CAAC,OACI,GAAI,EAAK,SAAW,GAAK,EAAK,EAAE,CAAC,OAAS,kBAAmB,CAKlE,IAAM,GAHY,EAAK,EACG,CAAC,UAAY,CAAC,EAAA,CACV,IAAK,GAAgB,EAAW,QAAQ,CAAE,CAC5C,CAAC,CAAC,KAAK,IAAI,EACvC,EAAY,KAAK,CACf,UAAW,eACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,UAAU,EAAS,EAAE,CACtD,CACF,CAAC,CACH,MAAO,GAAI,EAAK,SAAW,EAAG,CAE5B,IAAM,EAAU,EAAW,QAAQ,EAAK,EAAE,EAC1C,EAAY,KAAK,CACf,UAAW,iBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,OAAO,EAAQ,EAAE,CAClD,CACF,CAAC,CACH,MAEE,EAAY,KAAK,CACf,UAAW,kBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,aAAa,CAC9C,CACF,CAAC,EAGE,EAA0B,GAC7B,EAAY,KAAK,CACf,UAAW,mBACX,KAAM,CAAE,OAAQ,KAAM,EACtB,IAAK,EAAkB,EAAY,KAAK,CAC1C,CAAC,EAGH,EAAQ,OAAO,CACb,OACA,UAAW,2BACX,QAAS,CACX,CAAC,CACH,EAEA,gBAAgB,EAAe,CAG7B,GAFI,GAAgB,EAAa,GAE7B,CAAC,EAAK,SAAU,OAEpB,IAAM,EAAa,EAAQ,WAM3B,IALiB,EAAK,SAAS,OAAS,aAAe,EAAK,SAAS,KAAO,EAAW,QAAQ,EAAK,QAAQ,KAE3F,OAGb,EAA0B,EAAG,OAGjC,IAAM,EAAgB,EAAK,gBAAgB,SAAS,IAAM,EAAK,eAAe,SAAS,GACjF,EAAY,EAAgB,EAAW,QAAQ,CAAa,EAAI,IAEhE,EAAiD,CACrD,CACE,UAAW,mBACX,KAAM,CAAE,OAAQ,KAAM,EACtB,IAAK,EAAkB,EAAY,KAAK,CAC1C,CACF,EAEA,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,KAAM,CAAU,EACxB,QAAS,CACX,CAAC,CACH,CACF,CACF,CACF"}
@@ -1,2 +1,2 @@
1
- import{getFunctypeImportsLegacy as e,isFunctypeCall as t}from"../utils/functype-detection.js";import{createImportFixer as n,hasFunctypeSymbol as r}from"../utils/import-fixer.js";const i={meta:{type:`suggestion`,hasSuggestions:!0,docs:{description:`Prefer List<T> over native arrays for immutable collections`,recommended:!0},schema:[{type:`object`,properties:{allowArraysInTests:{type:`boolean`,default:!0},allowReadonlyArrays:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferList:`Prefer List<{{type}}> over array type {{arrayType}}`,preferListLiteral:`Prefer List.of(...) or List.from([...]) over array literal`,suggestListType:`Replace with List<{{type}}>`,suggestListOf:`Replace with List.of(...)`,suggestAddImport:`Add {{symbol}} import from functype`}},create(i){let a=i.options[0]||{},o=a.allowArraysInTests!==!1,s=a.allowReadonlyArrays!==!1,c=e(i);function l(){let e=i.filename;return/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(e)||e.includes(`__tests__`)||e.includes(`/test/`)||e.includes(`/tests/`)}function u(e,t){function n(e){if(e.type===`TSTypeParameterInstantiation`&&e.params&&e.params[0])return t.getText(e.params[0]);for(let t in e){if(t===`parent`)continue;let r=e[t];if(Array.isArray(r)){for(let e of r)if(e&&typeof e==`object`&&e.type){let t=n(e);if(t)return t}}else if(r&&typeof r==`object`&&r.type){let e=n(r);if(e)return e}}return null}return n(e)}return{TSArrayType(e){if(o&&l())return;let t=i.sourceCode,a=t.getText(e.elementType),s=t.getText(e),c=[{messageId:`suggestListType`,data:{type:a},fix(t){return t.replaceText(e,`List<${a}>`)}}];r(t,`List`)||c.push({messageId:`suggestAddImport`,data:{symbol:`List`},fix:n(t,`List`)}),i.report({node:e,messageId:`preferList`,data:{type:a,arrayType:s},suggest:c})},TSTypeReference(e){if(o&&l())return;let t=i.sourceCode;if(!e.typeName)return;let a=e.typeName.type===`Identifier`?e.typeName.name:t.getText(e.typeName);if(a===`Array`){let a=u(e,t),o=t.getText(e);if(s&&o.startsWith(`readonly`))return;let c=a||`T`,l=[{messageId:`suggestListType`,data:{type:c},fix(t){return t.replaceText(e,`List<${c}>`)}}];r(t,`List`)||l.push({messageId:`suggestAddImport`,data:{symbol:`List`},fix:n(t,`List`)}),i.report({node:e,messageId:`preferList`,data:{type:c,arrayType:o},suggest:l})}if(a===`ReadonlyArray`){let a=u(e,t),o=t.getText(e),s=a||`T`,c=[{messageId:`suggestListType`,data:{type:s},fix(t){return t.replaceText(e,`List<${s}>`)}}];r(t,`List`)||c.push({messageId:`suggestAddImport`,data:{symbol:`List`},fix:n(t,`List`)}),i.report({node:e,messageId:`preferList`,data:{type:s,arrayType:o},suggest:c})}},ArrayExpression(e){if(o&&l()||e.elements.length===0)return;let a=e.parent;if(a&&t(a,c)||a&&a.type===`CallExpression`&&a.callee.type===`MemberExpression`&&a.callee.object.type===`Identifier`&&a.callee.object.name===`List`&&[`from`,`of`].includes(a.callee.property.name))return;for(a=e.parent;a;){if(a.type===`ArrayExpression`)return;a=a.parent}let s=!1;for(a=e.parent;a;){if(a.type===`VariableDeclarator`&&a.id?.typeAnnotation){s=!0;break}if(a.type===`TSTypeAnnotation`){s=!0;break}a=a.parent}if(s)return;if(e.elements.some(e=>e!==null&&e.type===`SpreadElement`)){i.report({node:e,messageId:`preferListLiteral`});return}let u=i.sourceCode,d=e.elements.filter(e=>e!==null).map(e=>u.getText(e)),f=[{messageId:`suggestListOf`,fix(t){return t.replaceText(e,`List.of(${d.join(`, `)})`)}}];r(u,`List`)||f.push({messageId:`suggestAddImport`,data:{symbol:`List`},fix:n(u,`List`)}),i.report({node:e,messageId:`preferListLiteral`,suggest:f})}}}};export{i as default};
1
+ import{getFunctypeImportsLegacy as e,isFunctypeCall as t}from"../utils/functype-detection.js";import{createImportFixer as n,hasFunctypeSymbol as r}from"../utils/import-fixer.js";const i=new Set([`parent`,`loc`,`range`]);function a(e){return Object.entries(e).filter(([e])=>!i.has(e)).flatMap(([,e])=>Array.isArray(e)?e:[e]).filter(e=>typeof e==`object`&&!!e)}function o(e,t){let n=e?.parent;return n?t(n)||o(n,t):!1}function s(e){return e?.type===`CallExpression`&&e.callee.type===`MemberExpression`&&e.callee.object.type===`Identifier`&&e.callee.object.name===`List`&&[`from`,`of`].includes(e.callee.property.name)}function c(e){return e.type===`VariableDeclarator`&&!!e.id?.typeAnnotation||e.type===`TSTypeAnnotation`}const l={meta:{type:`suggestion`,hasSuggestions:!0,docs:{description:`Prefer List<T> over native arrays for immutable collections`,recommended:!0},schema:[{type:`object`,properties:{allowArraysInTests:{type:`boolean`,default:!0},allowReadonlyArrays:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferList:`Prefer List<{{type}}> over array type {{arrayType}}`,preferListLiteral:`Prefer List.of(...) or List.from([...]) over array literal`,suggestListType:`Replace with List<{{type}}>`,suggestListOf:`Replace with List.of(...)`,suggestAddImport:`Add {{symbol}} import from functype`}},create(i){let l=i.options[0]||{},u=l.allowArraysInTests!==!1,d=l.allowReadonlyArrays!==!1,f=e(i);function p(){let e=i.filename;return/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(e)||e.includes(`__tests__`)||e.includes(`/test/`)||e.includes(`/tests/`)}function m(e,t){function n(e){return e.type===`TSTypeParameterInstantiation`&&e.params&&e.params[0]?t.getText(e.params[0]):a(e).filter(e=>typeof e?.type==`string`).map(e=>n(e)).find(e=>e!==null)??null}return n(e)}return{TSArrayType(e){if(u&&p())return;let t=i.sourceCode,a=t.getText(e.elementType),o=t.getText(e),s=[{messageId:`suggestListType`,data:{type:a},fix(t){return t.replaceText(e,`List<${a}>`)}}];r(t,`List`)||s.push({messageId:`suggestAddImport`,data:{symbol:`List`},fix:n(t,`List`)}),i.report({node:e,messageId:`preferList`,data:{type:a,arrayType:o},suggest:s})},TSTypeReference(e){if(u&&p())return;let t=i.sourceCode;if(!e.typeName)return;let a=e.typeName.type===`Identifier`?e.typeName.name:t.getText(e.typeName);if(a===`Array`){let a=m(e,t),o=t.getText(e);if(d&&o.startsWith(`readonly`))return;let s=a||`T`,c=[{messageId:`suggestListType`,data:{type:s},fix(t){return t.replaceText(e,`List<${s}>`)}}];r(t,`List`)||c.push({messageId:`suggestAddImport`,data:{symbol:`List`},fix:n(t,`List`)}),i.report({node:e,messageId:`preferList`,data:{type:s,arrayType:o},suggest:c})}if(a===`ReadonlyArray`){let a=m(e,t),o=t.getText(e),s=a||`T`,c=[{messageId:`suggestListType`,data:{type:s},fix(t){return t.replaceText(e,`List<${s}>`)}}];r(t,`List`)||c.push({messageId:`suggestAddImport`,data:{symbol:`List`},fix:n(t,`List`)}),i.report({node:e,messageId:`preferList`,data:{type:s,arrayType:o},suggest:c})}},ArrayExpression(e){if(u&&p()||e.elements.length===0)return;let a=e.parent;if(a&&t(a,f)||s(a)||o(e,e=>e.type===`ArrayExpression`)||o(e,c))return;if(e.elements.some(e=>e!==null&&e.type===`SpreadElement`)){i.report({node:e,messageId:`preferListLiteral`});return}let l=i.sourceCode,d=e.elements.filter(e=>e!==null).map(e=>l.getText(e)),m=[{messageId:`suggestListOf`,fix(t){return t.replaceText(e,`List.of(${d.join(`, `)})`)}}];r(l,`List`)||m.push({messageId:`suggestAddImport`,data:{symbol:`List`},fix:n(l,`List`)}),i.report({node:e,messageId:`preferListLiteral`,suggest:m})}}}};export{l as default};
2
2
  //# sourceMappingURL=prefer-list.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-list.js","names":[],"sources":["../../src/rules/prefer-list.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImportsLegacy, isFunctypeCall } from \"../utils/functype-detection\"\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 List<T> over native arrays for immutable collections\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowArraysInTests: {\n type: \"boolean\",\n default: true,\n },\n allowReadonlyArrays: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferList: \"Prefer List<{{type}}> over array type {{arrayType}}\",\n preferListLiteral: \"Prefer List.of(...) or List.from([...]) over array literal\",\n suggestListType: \"Replace with List<{{type}}>\",\n suggestListOf: \"Replace with List.of(...)\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const allowArraysInTests = options.allowArraysInTests !== false\n const allowReadonlyArrays = options.allowReadonlyArrays !== false\n\n // Get functype imports if available (but still apply rule even without explicit import)\n const functypeImports = getFunctypeImportsLegacy(context)\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 findTypeParameter(node: ASTNode, sourceCode: typeof context.sourceCode): string | null {\n // Look for TSTypeParameterInstantiation child\n function findInNode(n: ASTNode): string | null {\n if (n.type === \"TSTypeParameterInstantiation\" && n.params && n.params[0]) {\n return sourceCode.getText(n.params[0])\n }\n\n // Recursively search child nodes\n for (const key in n) {\n if (key === \"parent\") continue\n const child = n[key]\n if (Array.isArray(child)) {\n for (const item of child) {\n if (item && typeof item === \"object\" && item.type) {\n const result = findInNode(item)\n if (result) return result\n }\n }\n } else if (child && typeof child === \"object\" && child.type) {\n const result = findInNode(child)\n if (result) return result\n }\n }\n\n return null\n }\n\n return findInNode(node)\n }\n\n return {\n TSArrayType(node: ASTNode) {\n if (allowArraysInTests && isInTestFile()) return\n\n const sourceCode = context.sourceCode\n const elementType = sourceCode.getText(node.elementType)\n const fullType = sourceCode.getText(node)\n\n const suggest: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestListType\",\n data: { type: elementType },\n fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, `List<${elementType}>`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"List\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"List\" },\n fix: createImportFixer(sourceCode, \"List\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferList\",\n data: {\n type: elementType,\n arrayType: fullType,\n },\n suggest,\n })\n },\n\n TSTypeReference(node: ASTNode) {\n if (allowArraysInTests && isInTestFile()) return\n\n const sourceCode = context.sourceCode\n\n // Get type name - handle both simple identifiers and member expressions\n if (!node.typeName) return // No type name found\n\n const typeName = node.typeName.type === \"Identifier\" ? node.typeName.name : sourceCode.getText(node.typeName)\n\n // Handle Array<T> syntax\n if (typeName === \"Array\") {\n // Look for type parameters in child nodes\n const typeParam = findTypeParameter(node, sourceCode)\n const fullType = sourceCode.getText(node)\n\n // Skip if already readonly\n if (allowReadonlyArrays && fullType.startsWith(\"readonly\")) return\n\n const resolvedType = typeParam || \"T\"\n\n const suggest: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestListType\",\n data: { type: resolvedType },\n fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, `List<${resolvedType}>`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"List\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"List\" },\n fix: createImportFixer(sourceCode, \"List\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferList\",\n data: {\n type: resolvedType,\n arrayType: fullType,\n },\n suggest,\n })\n }\n\n // Handle ReadonlyArray<T> - suggest List even if allowing readonly arrays\n if (typeName === \"ReadonlyArray\") {\n const typeParam = findTypeParameter(node, sourceCode)\n const fullType = sourceCode.getText(node)\n const resolvedType = typeParam || \"T\"\n\n const suggest: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestListType\",\n data: { type: resolvedType },\n fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, `List<${resolvedType}>`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"List\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"List\" },\n fix: createImportFixer(sourceCode, \"List\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferList\",\n data: {\n type: resolvedType,\n arrayType: fullType,\n },\n suggest,\n })\n }\n },\n\n ArrayExpression(node: ASTNode) {\n if (allowArraysInTests && isInTestFile()) return\n\n // Only flag non-empty arrays to avoid noise\n if (node.elements.length === 0) return\n\n // Don't flag arrays that are already arguments to List.from() or other functype calls\n let parent = node.parent\n if (parent && isFunctypeCall(parent, functypeImports)) {\n return\n }\n\n // Additional specific check for List.from/List.of patterns\n if (\n parent &&\n parent.type === \"CallExpression\" &&\n parent.callee.type === \"MemberExpression\" &&\n parent.callee.object.type === \"Identifier\" &&\n parent.callee.object.name === \"List\" &&\n [\"from\", \"of\"].includes(parent.callee.property.name)\n ) {\n return\n }\n\n // Don't flag nested array literals - only flag the outermost one\n // Check if this array is inside another array literal\n parent = node.parent\n while (parent) {\n if (parent.type === \"ArrayExpression\") {\n return // Skip nested arrays, let the outer one handle it\n }\n parent = parent.parent\n }\n\n // Don't flag array literals that are already part of a type annotation context\n // (those should be handled by the type checking rules)\n let hasTypeAnnotation = false\n parent = node.parent\n while (parent) {\n if (parent.type === \"VariableDeclarator\" && parent.id?.typeAnnotation) {\n // Skip array literal if there's already a type annotation that would be flagged\n hasTypeAnnotation = true\n break\n }\n if (parent.type === \"TSTypeAnnotation\") {\n hasTypeAnnotation = true\n break\n }\n parent = parent.parent\n }\n\n if (hasTypeAnnotation) return\n\n // Check if any element is a SpreadElement — ambiguous semantics, skip suggestions\n const hasSpread = node.elements.some((el) => el !== null && el.type === \"SpreadElement\")\n\n if (hasSpread) {\n context.report({\n node,\n messageId: \"preferListLiteral\",\n })\n return\n }\n\n const sourceCode = context.sourceCode\n const elementTexts = node.elements.filter((el) => el !== null).map((el) => sourceCode.getText(el))\n\n const suggest: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestListOf\",\n fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, `List.of(${elementTexts.join(\", \")})`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"List\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"List\" },\n fix: createImportFixer(sourceCode, \"List\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferListLiteral\",\n suggest,\n })\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
1
+ {"version":3,"file":"prefer-list.js","names":[],"sources":["../../src/rules/prefer-list.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImportsLegacy, isFunctypeCall } from \"../utils/functype-detection\"\nimport { createImportFixer, hasFunctypeSymbol } from \"../utils/import-fixer\"\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\n/** AST children of `node`: drops back-edges, flattens array-valued keys, filters non-object leaves. */\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/** True iff any ancestor of `node` satisfies `pred`. Pure tail recursion. */\nfunction ancestorSatisfies(node: ASTNode | null | undefined, pred: (n: ASTNode) => boolean): boolean {\n const parent = node?.parent as ASTNode | undefined\n if (!parent) return false\n return pred(parent) || ancestorSatisfies(parent, pred)\n}\n\n/** `parent` is `List.from(arr)` or `List.of(...arr)`. */\nfunction isListFactoryArg(parent: ASTNode | null | undefined): boolean {\n return (\n parent?.type === \"CallExpression\" &&\n parent.callee.type === \"MemberExpression\" &&\n parent.callee.object.type === \"Identifier\" &&\n parent.callee.object.name === \"List\" &&\n [\"from\", \"of\"].includes(parent.callee.property.name)\n )\n}\n\n/** `node` is a VariableDeclarator with its own type annotation, or a TSTypeAnnotation node directly. */\nfunction hasOwnTypeAnnotation(node: ASTNode): boolean {\n return (node.type === \"VariableDeclarator\" && Boolean(node.id?.typeAnnotation)) || node.type === \"TSTypeAnnotation\"\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n hasSuggestions: true,\n docs: {\n description: \"Prefer List<T> over native arrays for immutable collections\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowArraysInTests: {\n type: \"boolean\",\n default: true,\n },\n allowReadonlyArrays: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferList: \"Prefer List<{{type}}> over array type {{arrayType}}\",\n preferListLiteral: \"Prefer List.of(...) or List.from([...]) over array literal\",\n suggestListType: \"Replace with List<{{type}}>\",\n suggestListOf: \"Replace with List.of(...)\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const allowArraysInTests = options.allowArraysInTests !== false\n const allowReadonlyArrays = options.allowReadonlyArrays !== false\n\n // Get functype imports if available (but still apply rule even without explicit import)\n const functypeImports = getFunctypeImportsLegacy(context)\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 findTypeParameter(node: ASTNode, sourceCode: typeof context.sourceCode): string | null {\n // Pure pre-order search: look at this node, then recursively at each\n // child until we find a TSTypeParameterInstantiation with a first param.\n function findInNode(n: ASTNode): string | null {\n if (n.type === \"TSTypeParameterInstantiation\" && n.params && n.params[0]) {\n return sourceCode.getText(n.params[0])\n }\n const results = astChildren(n)\n .filter((c): c is ASTNode => typeof (c as ASTNode)?.type === \"string\")\n .map((child) => findInNode(child))\n return results.find((r) => r !== null) ?? null\n }\n return findInNode(node)\n }\n\n return {\n TSArrayType(node: ASTNode) {\n if (allowArraysInTests && isInTestFile()) return\n\n const sourceCode = context.sourceCode\n const elementType = sourceCode.getText(node.elementType)\n const fullType = sourceCode.getText(node)\n\n const suggest: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestListType\",\n data: { type: elementType },\n fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, `List<${elementType}>`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"List\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"List\" },\n fix: createImportFixer(sourceCode, \"List\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferList\",\n data: {\n type: elementType,\n arrayType: fullType,\n },\n suggest,\n })\n },\n\n TSTypeReference(node: ASTNode) {\n if (allowArraysInTests && isInTestFile()) return\n\n const sourceCode = context.sourceCode\n\n // Get type name - handle both simple identifiers and member expressions\n if (!node.typeName) return // No type name found\n\n const typeName = node.typeName.type === \"Identifier\" ? node.typeName.name : sourceCode.getText(node.typeName)\n\n // Handle Array<T> syntax\n if (typeName === \"Array\") {\n // Look for type parameters in child nodes\n const typeParam = findTypeParameter(node, sourceCode)\n const fullType = sourceCode.getText(node)\n\n // Skip if already readonly\n if (allowReadonlyArrays && fullType.startsWith(\"readonly\")) return\n\n const resolvedType = typeParam || \"T\"\n\n const suggest: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestListType\",\n data: { type: resolvedType },\n fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, `List<${resolvedType}>`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"List\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"List\" },\n fix: createImportFixer(sourceCode, \"List\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferList\",\n data: {\n type: resolvedType,\n arrayType: fullType,\n },\n suggest,\n })\n }\n\n // Handle ReadonlyArray<T> - suggest List even if allowing readonly arrays\n if (typeName === \"ReadonlyArray\") {\n const typeParam = findTypeParameter(node, sourceCode)\n const fullType = sourceCode.getText(node)\n const resolvedType = typeParam || \"T\"\n\n const suggest: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestListType\",\n data: { type: resolvedType },\n fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, `List<${resolvedType}>`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"List\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"List\" },\n fix: createImportFixer(sourceCode, \"List\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferList\",\n data: {\n type: resolvedType,\n arrayType: fullType,\n },\n suggest,\n })\n }\n },\n\n ArrayExpression(node: ASTNode) {\n if (allowArraysInTests && isInTestFile()) return\n\n // Only flag non-empty arrays to avoid noise\n if (node.elements.length === 0) return\n\n const parent = node.parent\n\n // Don't flag arrays that are already arguments to functype calls.\n if (parent && isFunctypeCall(parent, functypeImports)) return\n\n // Don't flag arrays that are arguments to List.from / List.of.\n if (isListFactoryArg(parent)) return\n\n // Don't flag nested array literals — let the outermost one handle it.\n if (ancestorSatisfies(node, (n) => n.type === \"ArrayExpression\")) return\n\n // Don't flag array literals that already live in a type-annotated\n // context (those are handled by the type-checking rules).\n if (ancestorSatisfies(node, hasOwnTypeAnnotation)) return\n\n // Check if any element is a SpreadElement — ambiguous semantics, skip suggestions\n const hasSpread = node.elements.some((el) => el !== null && el.type === \"SpreadElement\")\n\n if (hasSpread) {\n context.report({\n node,\n messageId: \"preferListLiteral\",\n })\n return\n }\n\n const sourceCode = context.sourceCode\n const elementTexts = node.elements.filter((el) => el !== null).map((el) => sourceCode.getText(el))\n\n const suggest: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestListOf\",\n fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, `List.of(${elementTexts.join(\", \")})`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"List\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"List\" },\n fix: createImportFixer(sourceCode, \"List\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferListLiteral\",\n suggest,\n })\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e={meta:{type:`suggestion`,docs:{description:`Prefer .map() over manual transformations and imperative patterns`,recommended:!0},fixable:`code`,schema:[{type:`object`,properties:{checkArrayMethods:{type:`boolean`,default:!0},checkForLoops:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferMapOverLoop:`Prefer .map() over for loop for transforming {{collection}}`,preferMapOverPush:`Prefer .map() over manual .push() in loop`,preferMapChain:`Consider using .map() for transformation instead of manual property access`}},create(e){let t=e.options[0]||{},n=t.checkArrayMethods!==!1,r=t.checkForLoops!==!1;function i(e){if(e.type!==`CallExpression`||e.callee.type!==`MemberExpression`||e.callee.property.name!==`forEach`)return!1;let t=e.arguments[0];return t&&(t.type===`ArrowFunctionExpression`||t.type===`FunctionExpression`)?t.body.type===`MemberExpression`:!1}function a(e){if(!e.body||e.body.type!==`BlockStatement`)return!1;let t=e.body.body;return t.length===0?!1:t.some(e=>{if(e.type===`ExpressionStatement`&&e.expression.type===`CallExpression`){let t=e.expression;return t.callee.type===`MemberExpression`&&t.callee.property.name===`push`}return!1})}function o(e){if(e.type===`CallExpression`&&e.callee.type===`MemberExpression`&&e.callee.property.name===`forEach`){let t=e.arguments[0];if(t&&(t.type===`ArrowFunctionExpression`||t.type===`FunctionExpression`)&&t.body.type===`MemberExpression`)return!0}return!1}return{ForStatement(t){r&&a(t)&&e.report({node:t,messageId:`preferMapOverLoop`,data:{collection:`array`}})},ForInStatement(t){r&&a(t)&&e.report({node:t,messageId:`preferMapOverLoop`,data:{collection:`object`}})},ForOfStatement(t){r&&a(t)&&e.report({node:t,messageId:`preferMapOverLoop`,data:{collection:`iterable`}})},CallExpression(t){if(n&&(t.callee.type===`MemberExpression`&&t.callee.property.name===`forEach`&&o(t)&&e.report({node:t,messageId:`preferMapChain`,fix(n){if(!i(t))return null;let r=e.sourceCode.getText(t).replace(/\.forEach\s*\(/g,`.map(`);return n.replaceText(t,r)}}),t.callee.type===`MemberExpression`&&t.callee.property.name===`push`)){let n=t.parent;for(;n;){if(n.type===`CallExpression`&&n.callee.type===`MemberExpression`&&(n.callee.property.name===`forEach`||n.callee.property.name===`for`)){e.report({node:t,messageId:`preferMapOverPush`});break}n=n.parent}}}}}};export{e as default};
1
+ const e=new Set([`forEach`,`for`]);function t(n){let r=n?.parent;return r?r.type===`CallExpression`&&r.callee.type===`MemberExpression`&&e.has(r.callee.property.name)?!0:t(r):!1}const n={meta:{type:`suggestion`,docs:{description:`Prefer .map() over manual transformations and imperative patterns`,recommended:!0},fixable:`code`,schema:[{type:`object`,properties:{checkArrayMethods:{type:`boolean`,default:!0},checkForLoops:{type:`boolean`,default:!0}},additionalProperties:!1}],messages:{preferMapOverLoop:`Prefer .map() over for loop for transforming {{collection}}`,preferMapOverPush:`Prefer .map() over manual .push() in loop`,preferMapChain:`Consider using .map() for transformation instead of manual property access`}},create(e){let n=e.options[0]||{},r=n.checkArrayMethods!==!1,i=n.checkForLoops!==!1;function a(e){if(e.type!==`CallExpression`||e.callee.type!==`MemberExpression`||e.callee.property.name!==`forEach`)return!1;let t=e.arguments[0];return t&&(t.type===`ArrowFunctionExpression`||t.type===`FunctionExpression`)?t.body.type===`MemberExpression`:!1}function o(e){if(!e.body||e.body.type!==`BlockStatement`)return!1;let t=e.body.body;return t.length===0?!1:t.some(e=>{if(e.type===`ExpressionStatement`&&e.expression.type===`CallExpression`){let t=e.expression;return t.callee.type===`MemberExpression`&&t.callee.property.name===`push`}return!1})}function s(e){if(e.type===`CallExpression`&&e.callee.type===`MemberExpression`&&e.callee.property.name===`forEach`){let t=e.arguments[0];if(t&&(t.type===`ArrowFunctionExpression`||t.type===`FunctionExpression`)&&t.body.type===`MemberExpression`)return!0}return!1}return{ForStatement(t){i&&o(t)&&e.report({node:t,messageId:`preferMapOverLoop`,data:{collection:`array`}})},ForInStatement(t){i&&o(t)&&e.report({node:t,messageId:`preferMapOverLoop`,data:{collection:`object`}})},ForOfStatement(t){i&&o(t)&&e.report({node:t,messageId:`preferMapOverLoop`,data:{collection:`iterable`}})},CallExpression(n){r&&(n.callee.type===`MemberExpression`&&n.callee.property.name===`forEach`&&s(n)&&e.report({node:n,messageId:`preferMapChain`,fix(t){if(!a(n))return null;let r=e.sourceCode.getText(n).replace(/\.forEach\s*\(/g,`.map(`);return t.replaceText(n,r)}}),n.callee.type===`MemberExpression`&&n.callee.property.name===`push`&&t(n)&&e.report({node:n,messageId:`preferMapOverPush`}))}}}};export{n as default};
2
2
  //# sourceMappingURL=prefer-map.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-map.js","names":[],"sources":["../../src/rules/prefer-map.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer .map() over manual transformations and imperative patterns\",\n recommended: true,\n },\n fixable: \"code\",\n schema: [\n {\n type: \"object\",\n properties: {\n checkArrayMethods: {\n type: \"boolean\",\n default: true,\n },\n checkForLoops: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferMapOverLoop: \"Prefer .map() over for loop for transforming {{collection}}\",\n preferMapOverPush: \"Prefer .map() over manual .push() in loop\",\n preferMapChain: \"Consider using .map() for transformation instead of manual property access\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const checkArrayMethods = options.checkArrayMethods !== false\n const checkForLoops = options.checkForLoops !== false\n\n function isForEachToMapSafe(node: ASTNode): boolean {\n // Only auto-fix simple forEach → map transformations on expressions that return values\n // This is safe because both native arrays and functype Lists have these methods\n if (\n node.type !== \"CallExpression\" ||\n node.callee.type !== \"MemberExpression\" ||\n node.callee.property.name !== \"forEach\"\n ) {\n return false\n }\n\n // Check if the callback returns a value (simple property access pattern)\n const callback = node.arguments[0]\n if (callback && (callback.type === \"ArrowFunctionExpression\" || callback.type === \"FunctionExpression\")) {\n const body = callback.body\n // Simple property access in arrow function (e.g., item => item.name)\n return body.type === \"MemberExpression\"\n }\n return false\n }\n\n function isTransformationLoop(node: ASTNode): boolean {\n if (!node.body || node.body.type !== \"BlockStatement\") return false\n\n const statements = node.body.body\n if (statements.length === 0) return false\n\n // Look for patterns like: newArray.push(transform(item))\n return statements.some((stmt: ASTNode) => {\n if (stmt.type === \"ExpressionStatement\" && stmt.expression.type === \"CallExpression\") {\n const call = stmt.expression\n return call.callee.type === \"MemberExpression\" && call.callee.property.name === \"push\"\n }\n return false\n })\n }\n\n function isSimplePropertyAccess(node: ASTNode): boolean {\n // Check for patterns like: items.forEach(item => console.log(item.name))\n // These could often be: items.map(item => item.name)\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.name === \"forEach\"\n ) {\n const callback = node.arguments[0]\n if (callback && (callback.type === \"ArrowFunctionExpression\" || callback.type === \"FunctionExpression\")) {\n const body = callback.body\n // Simple property access in arrow function\n if (body.type === \"MemberExpression\") {\n return true\n }\n }\n }\n return false\n }\n\n return {\n ForStatement(node: ASTNode) {\n if (!checkForLoops) return\n\n if (isTransformationLoop(node)) {\n context.report({\n node,\n messageId: \"preferMapOverLoop\",\n data: { collection: \"array\" },\n })\n }\n },\n\n ForInStatement(node: ASTNode) {\n if (!checkForLoops) return\n\n if (isTransformationLoop(node)) {\n context.report({\n node,\n messageId: \"preferMapOverLoop\",\n data: { collection: \"object\" },\n })\n }\n },\n\n ForOfStatement(node: ASTNode) {\n if (!checkForLoops) return\n\n if (isTransformationLoop(node)) {\n context.report({\n node,\n messageId: \"preferMapOverLoop\",\n data: { collection: \"iterable\" },\n })\n }\n },\n\n CallExpression(node: ASTNode) {\n if (!checkArrayMethods) return\n\n // Check for forEach that could be map\n if (node.callee.type === \"MemberExpression\" && node.callee.property.name === \"forEach\") {\n if (isSimplePropertyAccess(node)) {\n context.report({\n node,\n messageId: \"preferMapChain\",\n fix(fixer) {\n // Only auto-fix safe forEach → map transformations\n if (!isForEachToMapSafe(node)) {\n return null\n }\n\n const sourceCode = context.sourceCode\n const text = sourceCode.getText(node)\n\n // Simple replacement: forEach -> map\n const fixedText = text.replace(/\\.forEach\\s*\\(/g, \".map(\")\n return fixer.replaceText(node, fixedText)\n },\n })\n }\n }\n\n // Check for manual push patterns in callbacks\n if (node.callee.type === \"MemberExpression\" && node.callee.property.name === \"push\") {\n // Check if we're inside a forEach or similar iteration\n let parent = node.parent\n while (parent) {\n if (\n parent.type === \"CallExpression\" &&\n parent.callee.type === \"MemberExpression\" &&\n (parent.callee.property.name === \"forEach\" || parent.callee.property.name === \"for\")\n ) {\n context.report({\n node,\n messageId: \"preferMapOverPush\",\n })\n break\n }\n parent = parent.parent\n }\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
1
+ {"version":3,"file":"prefer-map.js","names":[],"sources":["../../src/rules/prefer-map.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\n\nconst ITERATION_METHOD_NAMES: ReadonlySet<string> = new Set([\"forEach\", \"for\"])\n\n/** True iff any ancestor of `node` is a `.forEach(...)` or `.for(...)` call. */\nfunction insideIterationCallback(node: ASTNode | null | undefined): boolean {\n const parent = node?.parent as ASTNode | undefined\n if (!parent) return false\n if (\n parent.type === \"CallExpression\" &&\n parent.callee.type === \"MemberExpression\" &&\n ITERATION_METHOD_NAMES.has(parent.callee.property.name)\n ) {\n return true\n }\n return insideIterationCallback(parent)\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer .map() over manual transformations and imperative patterns\",\n recommended: true,\n },\n fixable: \"code\",\n schema: [\n {\n type: \"object\",\n properties: {\n checkArrayMethods: {\n type: \"boolean\",\n default: true,\n },\n checkForLoops: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferMapOverLoop: \"Prefer .map() over for loop for transforming {{collection}}\",\n preferMapOverPush: \"Prefer .map() over manual .push() in loop\",\n preferMapChain: \"Consider using .map() for transformation instead of manual property access\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const checkArrayMethods = options.checkArrayMethods !== false\n const checkForLoops = options.checkForLoops !== false\n\n function isForEachToMapSafe(node: ASTNode): boolean {\n // Only auto-fix simple forEach → map transformations on expressions that return values\n // This is safe because both native arrays and functype Lists have these methods\n if (\n node.type !== \"CallExpression\" ||\n node.callee.type !== \"MemberExpression\" ||\n node.callee.property.name !== \"forEach\"\n ) {\n return false\n }\n\n // Check if the callback returns a value (simple property access pattern)\n const callback = node.arguments[0]\n if (callback && (callback.type === \"ArrowFunctionExpression\" || callback.type === \"FunctionExpression\")) {\n const body = callback.body\n // Simple property access in arrow function (e.g., item => item.name)\n return body.type === \"MemberExpression\"\n }\n return false\n }\n\n function isTransformationLoop(node: ASTNode): boolean {\n if (!node.body || node.body.type !== \"BlockStatement\") return false\n\n const statements = node.body.body\n if (statements.length === 0) return false\n\n // Look for patterns like: newArray.push(transform(item))\n return statements.some((stmt: ASTNode) => {\n if (stmt.type === \"ExpressionStatement\" && stmt.expression.type === \"CallExpression\") {\n const call = stmt.expression\n return call.callee.type === \"MemberExpression\" && call.callee.property.name === \"push\"\n }\n return false\n })\n }\n\n function isSimplePropertyAccess(node: ASTNode): boolean {\n // Check for patterns like: items.forEach(item => console.log(item.name))\n // These could often be: items.map(item => item.name)\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.name === \"forEach\"\n ) {\n const callback = node.arguments[0]\n if (callback && (callback.type === \"ArrowFunctionExpression\" || callback.type === \"FunctionExpression\")) {\n const body = callback.body\n // Simple property access in arrow function\n if (body.type === \"MemberExpression\") {\n return true\n }\n }\n }\n return false\n }\n\n return {\n ForStatement(node: ASTNode) {\n if (!checkForLoops) return\n\n if (isTransformationLoop(node)) {\n context.report({\n node,\n messageId: \"preferMapOverLoop\",\n data: { collection: \"array\" },\n })\n }\n },\n\n ForInStatement(node: ASTNode) {\n if (!checkForLoops) return\n\n if (isTransformationLoop(node)) {\n context.report({\n node,\n messageId: \"preferMapOverLoop\",\n data: { collection: \"object\" },\n })\n }\n },\n\n ForOfStatement(node: ASTNode) {\n if (!checkForLoops) return\n\n if (isTransformationLoop(node)) {\n context.report({\n node,\n messageId: \"preferMapOverLoop\",\n data: { collection: \"iterable\" },\n })\n }\n },\n\n CallExpression(node: ASTNode) {\n if (!checkArrayMethods) return\n\n // Check for forEach that could be map\n if (node.callee.type === \"MemberExpression\" && node.callee.property.name === \"forEach\") {\n if (isSimplePropertyAccess(node)) {\n context.report({\n node,\n messageId: \"preferMapChain\",\n fix(fixer) {\n // Only auto-fix safe forEach → map transformations\n if (!isForEachToMapSafe(node)) {\n return null\n }\n\n const sourceCode = context.sourceCode\n const text = sourceCode.getText(node)\n\n // Simple replacement: forEach -> map\n const fixedText = text.replace(/\\.forEach\\s*\\(/g, \".map(\")\n return fixer.replaceText(node, fixedText)\n },\n })\n }\n }\n\n // Check for manual push patterns in callbacks\n if (\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.name === \"push\" &&\n insideIterationCallback(node)\n ) {\n context.report({ node, messageId: \"preferMapOverPush\" })\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { Rule } from "eslint";
2
+
3
+ //#region src/rules/prefer-try.d.ts
4
+ declare const rule: Rule.RuleModule;
5
+ //#endregion
6
+ export { rule as default };
7
+ //# sourceMappingURL=prefer-try.d.ts.map
@@ -0,0 +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};
2
+ //# sourceMappingURL=prefer-try.js.map
@@ -0,0 +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 +1,4 @@
1
- import{n as e,r as t,t as n}from"../dependency-validator-Cnms4306.js";export{n as createValidationError,e as shouldValidateDependencies,t as validatePeerDependencies};
1
+ import{createRequire as e}from"node:module";var t=e(import.meta.url);const n=[{name:`@typescript-eslint/eslint-plugin`,packageName:`@typescript-eslint/eslint-plugin`,description:`TypeScript-aware ESLint rules`,required:!0},{name:`@typescript-eslint/parser`,packageName:`@typescript-eslint/parser`,description:`TypeScript parser for ESLint`,required:!0},{name:`eslint-plugin-functional`,packageName:`eslint-plugin-functional`,description:`Functional programming ESLint rules`,required:!0},{name:`eslint-plugin-prettier`,packageName:`eslint-plugin-prettier`,description:`Code formatting rules`,required:!1},{name:`eslint-plugin-simple-import-sort`,packageName:`eslint-plugin-simple-import-sort`,description:`Import sorting rules`,required:!1},{name:`prettier`,packageName:`prettier`,description:`Code formatter`,required:!1}];function r(e){try{return t.resolve(e),!0}catch{return!1}}function i(){let[e,t]=n.reduce(([e,t],n)=>r(n.packageName)?[[...e,n],t]:[e,[...t,n]],[[],[]]),i=t.filter(e=>!e.required).map(e=>`Optional plugin '${e.name}' not found. Some rules will be skipped.`),a=t.filter(e=>e.required),o=t.length>0?`pnpm add -D ${t.map(e=>e.packageName).join(` `)}`:``;return{isValid:a.length===0,missing:t,available:e,installCommand:o,warnings:i}}function a(e){let t=e.missing.filter(e=>e.required);if(t.length===0)return Error(`No validation errors`);let n=[`❌ Missing required peer dependencies for eslint-plugin-functype:`,``,t.map(e=>` • ${e.name} - ${e.description}`).join(`
2
+ `),``,`📦 Install missing dependencies:`,` ${e.installCommand}`,``,`📖 See installation guide: https://github.com/jordanburke/eslint-plugin-functype#installation`].join(`
3
+ `);return Error(n)}function o(){return process.env.NODE_ENV!==`test`&&process.env.FUNCTYPE_SKIP_VALIDATION!==`true`}export{a as createValidationError,o as shouldValidateDependencies,i as validatePeerDependencies};
4
+ //# sourceMappingURL=dependency-validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependency-validator.js","names":[],"sources":["../../src/utils/dependency-validator.ts"],"sourcesContent":["// Utility to validate peer dependencies and provide helpful error messages\n\ninterface PeerDependency {\n name: string\n packageName: string\n description: string\n required: boolean\n}\n\nconst PEER_DEPENDENCIES: PeerDependency[] = [\n {\n name: \"@typescript-eslint/eslint-plugin\",\n packageName: \"@typescript-eslint/eslint-plugin\",\n description: \"TypeScript-aware ESLint rules\",\n required: true,\n },\n {\n name: \"@typescript-eslint/parser\",\n packageName: \"@typescript-eslint/parser\",\n description: \"TypeScript parser for ESLint\",\n required: true,\n },\n {\n name: \"eslint-plugin-functional\",\n packageName: \"eslint-plugin-functional\",\n description: \"Functional programming ESLint rules\",\n required: true,\n },\n {\n name: \"eslint-plugin-prettier\",\n packageName: \"eslint-plugin-prettier\",\n description: \"Code formatting rules\",\n required: false,\n },\n {\n name: \"eslint-plugin-simple-import-sort\",\n packageName: \"eslint-plugin-simple-import-sort\",\n description: \"Import sorting rules\",\n required: false,\n },\n {\n name: \"prettier\",\n packageName: \"prettier\",\n description: \"Code formatter\",\n required: false,\n },\n]\n\nexport interface ValidationResult {\n isValid: boolean\n missing: PeerDependency[]\n available: PeerDependency[]\n installCommand: string\n warnings: string[]\n}\n\nfunction tryRequire(packageName: string): boolean {\n try {\n require.resolve(packageName)\n return true\n } catch {\n return false\n }\n}\n\nexport function validatePeerDependencies(): ValidationResult {\n // Partition each dep into present/absent up-front; downstream calcs are\n // pure derivations from those two arrays.\n const [available, missing] = PEER_DEPENDENCIES.reduce<[PeerDependency[], PeerDependency[]]>(\n ([ok, bad], dep) => (tryRequire(dep.packageName) ? [[...ok, dep], bad] : [ok, [...bad, dep]]),\n [[], []],\n )\n\n const warnings = missing\n .filter((dep) => !dep.required)\n .map((dep) => `Optional plugin '${dep.name}' not found. Some rules will be skipped.`)\n\n const requiredMissing = missing.filter((dep) => dep.required)\n const installCommand = missing.length > 0 ? `pnpm add -D ${missing.map((dep) => dep.packageName).join(\" \")}` : \"\"\n\n return {\n isValid: requiredMissing.length === 0,\n missing,\n available,\n installCommand,\n warnings,\n }\n}\n\nexport function createValidationError(result: ValidationResult): Error {\n const requiredMissing = result.missing.filter((dep) => dep.required)\n\n if (requiredMissing.length === 0) {\n return new Error(\"No validation errors\")\n }\n\n const missingList = requiredMissing.map((dep) => ` • ${dep.name} - ${dep.description}`).join(\"\\n\")\n\n const message = [\n \"❌ Missing required peer dependencies for eslint-plugin-functype:\",\n \"\",\n missingList,\n \"\",\n \"📦 Install missing dependencies:\",\n ` ${result.installCommand}`,\n \"\",\n \"📖 See installation guide: https://github.com/jordanburke/eslint-plugin-functype#installation\",\n ].join(\"\\n\")\n\n return new Error(message)\n}\n\nexport function shouldValidateDependencies(): boolean {\n // Skip validation in test environments or when explicitly disabled\n return process.env.NODE_ENV !== \"test\" && process.env.FUNCTYPE_SKIP_VALIDATION !== \"true\"\n}\n"],"mappings":"qEASA,MAAM,EAAsC,CAC1C,CACE,KAAM,mCACN,YAAa,mCACb,YAAa,gCACb,SAAU,EACZ,EACA,CACE,KAAM,4BACN,YAAa,4BACb,YAAa,+BACb,SAAU,EACZ,EACA,CACE,KAAM,2BACN,YAAa,2BACb,YAAa,sCACb,SAAU,EACZ,EACA,CACE,KAAM,yBACN,YAAa,yBACb,YAAa,wBACb,SAAU,EACZ,EACA,CACE,KAAM,mCACN,YAAa,mCACb,YAAa,uBACb,SAAU,EACZ,EACA,CACE,KAAM,WACN,YAAa,WACb,YAAa,iBACb,SAAU,EACZ,CACF,EAUA,SAAS,EAAW,EAA8B,CAChD,GAAI,CAEF,OADA,EAAQ,QAAQ,CAAW,EACpB,EACT,MAAQ,CACN,MAAO,EACT,CACF,CAEA,SAAgB,GAA6C,CAG3D,GAAM,CAAC,EAAW,GAAW,EAAkB,QAC5C,CAAC,EAAI,GAAM,IAAS,EAAW,EAAI,WAAW,EAAI,CAAC,CAAC,GAAG,EAAI,CAAG,EAAG,CAAG,EAAI,CAAC,EAAI,CAAC,GAAG,EAAK,CAAG,CAAC,EAC3F,CAAC,CAAC,EAAG,CAAC,CAAC,CACT,EAEM,EAAW,EACd,OAAQ,GAAQ,CAAC,EAAI,QAAQ,CAAC,CAC9B,IAAK,GAAQ,oBAAoB,EAAI,KAAK,yCAAyC,EAEhF,EAAkB,EAAQ,OAAQ,GAAQ,EAAI,QAAQ,EACtD,EAAiB,EAAQ,OAAS,EAAI,eAAe,EAAQ,IAAK,GAAQ,EAAI,WAAW,CAAC,CAAC,KAAK,GAAG,IAAM,GAE/G,MAAO,CACL,QAAS,EAAgB,SAAW,EACpC,UACA,YACA,iBACA,UACF,CACF,CAEA,SAAgB,EAAsB,EAAiC,CACrE,IAAM,EAAkB,EAAO,QAAQ,OAAQ,GAAQ,EAAI,QAAQ,EAEnE,GAAI,EAAgB,SAAW,EAC7B,OAAW,MAAM,sBAAsB,EAKzC,IAAM,EAAU,CACd,mEACA,GAJkB,EAAgB,IAAK,GAAQ,OAAO,EAAI,KAAK,KAAK,EAAI,aAAa,CAAC,CAAC,KAAK;CAKlF,EACV,GACA,mCACA,MAAM,EAAO,iBACb,GACA,+FACF,CAAC,CAAC,KAAK;CAAI,EAEX,OAAW,MAAM,CAAO,CAC1B,CAEA,SAAgB,GAAsC,CAEpD,OAAO,QAAQ,IAAI,WAAa,QAAU,QAAQ,IAAI,2BAA6B,MACrF"}
@@ -2,9 +2,6 @@ import { ASTNode } from "../types/ast.js";
2
2
  import { Rule } from "eslint";
3
3
 
4
4
  //#region src/utils/functype-detection.d.ts
5
- /**
6
- * Utility functions for detecting functype library usage in ESLint rules
7
- */
8
5
  /**
9
6
  * Check if functype library is imported in the current file
10
7
  */
@@ -1,2 +1,2 @@
1
- function e(e){let t=e.sourceCode.ast;for(let e of t.body)if(e.type===`ImportDeclaration`&&e.source.type===`Literal`&&e.source.value===`functype`)return!0;return!1}function t(e){let t=new Set,n=new Set,r=new Set,i=e.sourceCode.ast;for(let e of i.body)if(e.type===`ImportDeclaration`&&e.source.type===`Literal`&&e.source.value===`functype`&&e.specifiers)for(let i of e.specifiers)if(i.type===`ImportSpecifier`&&i.imported.type===`Identifier`){let e=i.imported.name;r.add(e),[`Option`,`Either`,`List`,`LazyList`,`Task`,`Try`,`Map`,`Set`,`Stack`].includes(e)?t.add(e):([`Do`,`DoAsync`,`$`].includes(e),n.add(e))}else i.type===`ImportDefaultSpecifier`?(r.add(`default`),n.add(`default`)):i.type===`ImportNamespaceSpecifier`&&(r.add(`*`),t.add(`*`),n.add(`*`));return{types:t,functions:n,all:r}}function n(e){return t(e).all}function r(e,t){if(!e)return!1;let n=t instanceof Set?t:t.types||t.all;if(e.type===`TSTypeReference`&&e.typeName?.type===`Identifier`){let t=e.typeName.name;return n.has(t)||[`Option`,`Either`,`List`,`LazyList`,`Task`,`Try`,`Map`,`Set`,`Stack`].includes(t)}return!1}function i(e,t){if(!e||e.type!==`CallExpression`)return!1;let n=e.callee;if(n.type===`MemberExpression`&&n.object.type===`Identifier`){let e=n.object.name,r=n.property?.name;if(t.has(e)||e===`Option`&&[`some`,`none`,`of`].includes(r)||e===`Either`&&[`left`,`right`,`of`].includes(r)||e===`List`&&[`of`,`from`,`empty`].includes(r)||e===`Map`&&[`of`,`empty`].includes(r)||e===`Set`&&[`of`,`empty`].includes(r))return!0}if(n.type===`MemberExpression`){let e=n.property?.name;if([`map`,`flatMap`,`filter`,`fold`,`foldLeft`,`foldRight`,`getOrElse`,`orElse`,`isEmpty`,`nonEmpty`,`isDefined`,`isSome`,`isNone`,`isLeft`,`isRight`,`toArray`].includes(e))return!0}return!1}function a(e,t){let n=e.parent;for(;n;){if(i(n,t)||r(n,t))return!0;n=n.parent}return!1}function o(e,t){return e.typeAnnotation?.typeAnnotation?r(e.typeAnnotation.typeAnnotation,t):!1}function s(e,t){if(!e||e.type!==`CallExpression`)return!1;let n=e.callee;if(n.type===`Identifier`){let e=n.name;return t.functions.has(e)&&[`Do`,`DoAsync`].includes(e)}return!1}function c(e,t){if(!e||e.type!==`CallExpression`)return!1;let n=e.callee;return n.type===`Identifier`&&n.name===`$`?t.functions.has(`$`):!1}function l(e,t){let n=e.parent;for(;n;){if(n.type===`CallExpression`&&s(n,t))return!0;n=n.parent}return!1}function u(e,t){if(!e||e.type!==`CallExpression`)return!1;let n=e.callee;return n.type===`MemberExpression`&&n.property.type===`Identifier`&&n.property.name===t}function d(e,t){if(l(e,t))return{shouldUse:!1,reason:null};if(e.type===`LogicalExpression`&&e.operator===`&&`){let t=e.toString?e.toString():``;if(t.includes(`&&`)&&t.match(/\w+(\.\w+){2,}/))return{shouldUse:!0,reason:`nested-checks`}}if(u(e,`flatMap`)){let t=0,n=e;for(;n&&n.type===`CallExpression`&&u(n,`flatMap`)&&(t++,n.callee.type===`MemberExpression`);)n=n.callee.object;if(t>=3)return{shouldUse:!0,reason:`chained-flatmaps`}}return{shouldUse:!1,reason:null}}export{t as getFunctypeImports,n as getFunctypeImportsLegacy,e as hasFunctypeImport,o as hasFunctypeTypeAnnotation,a as isAlreadyUsingFunctype,u as isChainedMethodCall,s as isDoNotationCall,c as isDollarHelper,i as isFunctypeCall,r as isFunctypeType,l as isInsideDoNotation,d as shouldUseDoNotation};
1
+ const e=new Set([`Option`,`Either`,`List`,`LazyList`,`Task`,`Try`,`Map`,`Set`,`Stack`]),t=e=>e.type===`ImportDeclaration`&&e.source.type===`Literal`&&e.source.value===`functype`;function n(e){return e.sourceCode.ast.body.some(t)}function r(t){if(t.type===`ImportSpecifier`&&t.imported.type===`Identifier`){let n=t.imported.name;return e.has(n)?{type:n,fn:null,all:n}:{type:null,fn:n,all:n}}return t.type===`ImportDefaultSpecifier`?{type:null,fn:`default`,all:`default`}:t.type===`ImportNamespaceSpecifier`?{type:`*`,fn:`*`,all:`*`}:null}const i=(e,t)=>e.flatMap(e=>{let n=e[t];return n===null?[]:[n]});function a(e){let n=e.sourceCode.ast.body.filter(t).flatMap(e=>e.specifiers??[]).map(r).filter(e=>e!==null);return{types:new Set(i(n,`type`)),functions:new Set(i(n,`fn`)),all:new Set(n.map(e=>e.all))}}function o(e){return a(e).all}function s(e,t){if(!e)return!1;let n=t instanceof Set?t:t.types||t.all;if(e.type===`TSTypeReference`&&e.typeName?.type===`Identifier`){let t=e.typeName.name;return n.has(t)||[`Option`,`Either`,`List`,`LazyList`,`Task`,`Try`,`Map`,`Set`,`Stack`].includes(t)}return!1}function c(e,t){if(!e||e.type!==`CallExpression`)return!1;let n=e.callee;if(n.type===`MemberExpression`&&n.object.type===`Identifier`){let e=n.object.name,r=n.property?.name;if(t.has(e)||e===`Option`&&[`some`,`none`,`of`].includes(r)||e===`Either`&&[`left`,`right`,`of`].includes(r)||e===`List`&&[`of`,`from`,`empty`].includes(r)||e===`Map`&&[`of`,`empty`].includes(r)||e===`Set`&&[`of`,`empty`].includes(r))return!0}if(n.type===`MemberExpression`){let e=n.property?.name;if([`map`,`flatMap`,`filter`,`fold`,`foldLeft`,`foldRight`,`getOrElse`,`orElse`,`isEmpty`,`nonEmpty`,`isDefined`,`isSome`,`isNone`,`isLeft`,`isRight`,`toArray`].includes(e))return!0}return!1}function l(e,t){let n=e?.parent;return n?t(n)||l(n,t):!1}function u(e,t){return l(e,e=>c(e,t)||s(e,t))}function d(e,t){return e.typeAnnotation?.typeAnnotation?s(e.typeAnnotation.typeAnnotation,t):!1}function f(e,t){if(!e||e.type!==`CallExpression`)return!1;let n=e.callee;if(n.type===`Identifier`){let e=n.name;return t.functions.has(e)&&[`Do`,`DoAsync`].includes(e)}return!1}function p(e,t){if(!e||e.type!==`CallExpression`)return!1;let n=e.callee;return n.type===`Identifier`&&n.name===`$`?t.functions.has(`$`):!1}function m(e,t){return l(e,e=>e.type===`CallExpression`&&f(e,t))}function h(e,t){if(!e||e.type!==`CallExpression`)return!1;let n=e.callee;return n.type===`MemberExpression`&&n.property.type===`Identifier`&&n.property.name===t}function g(e,t){if(m(e,t))return{shouldUse:!1,reason:null};if(e.type===`LogicalExpression`&&e.operator===`&&`){let t=e.toString?e.toString():``;if(t.includes(`&&`)&&t.match(/\w+(\.\w+){2,}/))return{shouldUse:!0,reason:`nested-checks`}}return h(e,`flatMap`)&&_(e)>=3?{shouldUse:!0,reason:`chained-flatmaps`}:{shouldUse:!1,reason:null}}function _(e){return!e||e.type!==`CallExpression`||!h(e,`flatMap`)?0:e.callee.type===`MemberExpression`?1+_(e.callee.object):1}export{a as getFunctypeImports,o as getFunctypeImportsLegacy,n as hasFunctypeImport,d as hasFunctypeTypeAnnotation,u as isAlreadyUsingFunctype,h as isChainedMethodCall,f as isDoNotationCall,p as isDollarHelper,c as isFunctypeCall,s as isFunctypeType,m as isInsideDoNotation,g as shouldUseDoNotation};
2
2
  //# sourceMappingURL=functype-detection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"functype-detection.js","names":[],"sources":["../../src/utils/functype-detection.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\n\n/**\n * Utility functions for detecting functype library usage in ESLint rules\n */\n\n/**\n * Check if functype library is imported in the current file\n */\nexport function hasFunctypeImport(context: Rule.RuleContext): boolean {\n const sourceCode = context.sourceCode\n const program = sourceCode.ast\n\n // Look for import statements that import from 'functype'\n for (const node of program.body) {\n if (node.type === \"ImportDeclaration\" && node.source.type === \"Literal\" && node.source.value === \"functype\") {\n return true\n }\n }\n\n return false\n}\n\n/**\n * Enhanced functype imports tracking\n */\nexport interface FunctypeImports {\n types: Set<string> // Option, Either, List, etc.\n functions: Set<string> // Do, DoAsync, $, etc.\n all: Set<string> // Combined for compatibility\n}\n\n/**\n * Get imported functype symbols from the current file\n */\nexport function getFunctypeImports(context: Rule.RuleContext): FunctypeImports {\n const types = new Set<string>()\n const functions = new Set<string>()\n const all = new Set<string>()\n\n const sourceCode = context.sourceCode\n const program = sourceCode.ast\n\n for (const node of program.body) {\n if (node.type === \"ImportDeclaration\" && node.source.type === \"Literal\" && node.source.value === \"functype\") {\n // Handle named imports: import { Option, Either, Do, $ } from 'functype'\n if (node.specifiers) {\n for (const spec of node.specifiers) {\n if (spec.type === \"ImportSpecifier\" && spec.imported.type === \"Identifier\") {\n const name = spec.imported.name\n all.add(name)\n\n // Categorize imports\n if ([\"Option\", \"Either\", \"List\", \"LazyList\", \"Task\", \"Try\", \"Map\", \"Set\", \"Stack\"].includes(name)) {\n types.add(name)\n } else if ([\"Do\", \"DoAsync\", \"$\"].includes(name)) {\n functions.add(name)\n } else {\n // Other imports (constructors, utilities, etc.)\n functions.add(name)\n }\n } else if (spec.type === \"ImportDefaultSpecifier\") {\n all.add(\"default\")\n functions.add(\"default\")\n } else if (spec.type === \"ImportNamespaceSpecifier\") {\n all.add(\"*\")\n types.add(\"*\")\n functions.add(\"*\")\n }\n }\n }\n }\n }\n\n return { types, functions, all }\n}\n\n/**\n * Legacy compatibility - returns the 'all' set\n */\nexport function getFunctypeImportsLegacy(context: Rule.RuleContext): Set<string> {\n return getFunctypeImports(context).all\n}\n\n/**\n * Check if a type reference is using functype types\n */\nexport function isFunctypeType(node: ASTNode, functypeImports: FunctypeImports | Set<string>): boolean {\n if (!node) return false\n\n // Handle both new and legacy API\n const types = functypeImports instanceof Set ? functypeImports : functypeImports.types || functypeImports.all\n\n // Check direct type names\n if (node.type === \"TSTypeReference\" && node.typeName?.type === \"Identifier\") {\n const typeName = node.typeName.name\n return (\n types.has(typeName) ||\n [\"Option\", \"Either\", \"List\", \"LazyList\", \"Task\", \"Try\", \"Map\", \"Set\", \"Stack\"].includes(typeName)\n )\n }\n\n return false\n}\n\n/**\n * Check if a call expression is using functype methods\n */\nexport function isFunctypeCall(node: ASTNode, functypeImports: Set<string>): boolean {\n if (!node || node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n\n // Check for static method calls like Option.some(), Either.left(), List.of()\n if (callee.type === \"MemberExpression\" && callee.object.type === \"Identifier\") {\n const objectName = callee.object.name\n const methodName = callee.property?.name\n\n // Check if calling methods on imported functype types\n if (functypeImports.has(objectName)) return true\n\n // Check for common functype patterns\n if (\n (objectName === \"Option\" && [\"some\", \"none\", \"of\"].includes(methodName)) ||\n (objectName === \"Either\" && [\"left\", \"right\", \"of\"].includes(methodName)) ||\n (objectName === \"List\" && [\"of\", \"from\", \"empty\"].includes(methodName)) ||\n (objectName === \"Map\" && [\"of\", \"empty\"].includes(methodName)) ||\n (objectName === \"Set\" && [\"of\", \"empty\"].includes(methodName))\n ) {\n return true\n }\n }\n\n // Check for method calls on functype instances like someOption.map()\n if (callee.type === \"MemberExpression\") {\n const methodName = callee.property?.name\n\n // Common functype methods\n if (\n [\n \"map\",\n \"flatMap\",\n \"filter\",\n \"fold\",\n \"foldLeft\",\n \"foldRight\",\n \"getOrElse\",\n \"orElse\",\n \"isEmpty\",\n \"nonEmpty\",\n \"isDefined\",\n \"isSome\",\n \"isNone\",\n \"isLeft\",\n \"isRight\",\n \"toArray\",\n ].includes(methodName)\n ) {\n return true\n }\n }\n\n return false\n}\n\n/**\n * Check if current context is already using functype patterns appropriately\n */\nexport function isAlreadyUsingFunctype(node: ASTNode, functypeImports: Set<string>): boolean {\n let parent = node.parent\n\n // Walk up the AST to find functype usage\n while (parent) {\n if (isFunctypeCall(parent as ASTNode, functypeImports) || isFunctypeType(parent as ASTNode, functypeImports)) {\n return true\n }\n parent = parent.parent\n }\n\n return false\n}\n\n/**\n * Check if a variable or parameter is typed with functype types\n */\nexport function hasFunctypeTypeAnnotation(node: ASTNode, functypeImports: FunctypeImports | Set<string>): boolean {\n // Check for type annotation\n if (node.typeAnnotation?.typeAnnotation) {\n return isFunctypeType(node.typeAnnotation.typeAnnotation, functypeImports)\n }\n\n return false\n}\n\n/**\n * Check if a call expression is a Do notation call\n */\nexport function isDoNotationCall(node: ASTNode, functypeImports: FunctypeImports): boolean {\n if (!node || node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n\n // Check for Do() or DoAsync() calls\n if (callee.type === \"Identifier\") {\n const name = callee.name\n return functypeImports.functions.has(name) && [\"Do\", \"DoAsync\"].includes(name)\n }\n\n return false\n}\n\n/**\n * Check if a call expression uses the $ helper function\n */\nexport function isDollarHelper(node: ASTNode, functypeImports: FunctypeImports): boolean {\n if (!node || node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n\n // Check for $() calls\n if (callee.type === \"Identifier\" && callee.name === \"$\") {\n return functypeImports.functions.has(\"$\")\n }\n\n return false\n}\n\n/**\n * Check if current expression is inside a Do notation block\n */\nexport function isInsideDoNotation(node: ASTNode, functypeImports: FunctypeImports): boolean {\n let current = node.parent\n\n while (current) {\n if (current.type === \"CallExpression\" && isDoNotationCall(current as ASTNode, functypeImports)) {\n return true\n }\n current = current.parent\n }\n\n return false\n}\n\n/**\n * Check if a method call is a chained method call on functype types\n */\nexport function isChainedMethodCall(node: ASTNode, methodName: string): boolean {\n if (!node || node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n\n if (\n callee.type === \"MemberExpression\" &&\n callee.property.type === \"Identifier\" &&\n callee.property.name === methodName\n ) {\n return true\n }\n\n return false\n}\n\n/**\n * Detect if code could benefit from Do notation\n */\nexport function shouldUseDoNotation(\n node: ASTNode,\n functypeImports: FunctypeImports,\n): {\n shouldUse: boolean\n reason: \"nested-checks\" | \"chained-flatmaps\" | \"mixed-monads\" | \"async-chains\" | null\n} {\n // Already using Do notation\n if (isInsideDoNotation(node, functypeImports)) {\n return { shouldUse: false, reason: null }\n }\n\n // Check for nested null checks (a && a.b && a.b.c pattern)\n if (node.type === \"LogicalExpression\" && node.operator === \"&&\") {\n const text = node.toString ? node.toString() : \"\"\n if (text.includes(\"&&\") && text.match(/\\w+(\\.\\w+){2,}/)) {\n return { shouldUse: true, reason: \"nested-checks\" }\n }\n }\n\n // Check for chained flatMap calls\n if (isChainedMethodCall(node, \"flatMap\")) {\n // Count chain depth\n let depth = 0\n let current = node\n\n while (current && current.type === \"CallExpression\" && isChainedMethodCall(current, \"flatMap\")) {\n depth++\n if (current.callee.type === \"MemberExpression\") {\n current = current.callee.object as ASTNode\n } else {\n break\n }\n }\n\n if (depth >= 3) {\n return { shouldUse: true, reason: \"chained-flatmaps\" }\n }\n }\n\n return { shouldUse: false, reason: null }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"functype-detection.js","names":[],"sources":["../../src/utils/functype-detection.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\n\n/**\n * Utility functions for detecting functype library usage in ESLint rules.\n *\n * Implementation style: pure helpers operating over native immutable\n * primitives (Array.prototype methods + ReadonlySet). No mutable\n * accumulators, no `for-of` driving state changes. The plugin's domain\n * (AST traversal) is naturally tree-shaped; the helpers compose as\n * folds/filters/maps over those trees.\n */\n\nconst TYPE_NAMES: ReadonlySet<string> = new Set([\n \"Option\",\n \"Either\",\n \"List\",\n \"LazyList\",\n \"Task\",\n \"Try\",\n \"Map\",\n \"Set\",\n \"Stack\",\n])\n\n/** `import ... from \"functype\"` — including `functype/<subpath>` */\nconst isFunctypeImport = (node: ASTNode): boolean =>\n node.type === \"ImportDeclaration\" && node.source.type === \"Literal\" && node.source.value === \"functype\"\n\n/**\n * Check if functype library is imported in the current file\n */\nexport function hasFunctypeImport(context: Rule.RuleContext): boolean {\n return context.sourceCode.ast.body.some(isFunctypeImport)\n}\n\n/**\n * Enhanced functype imports tracking\n */\nexport interface FunctypeImports {\n types: Set<string> // Option, Either, List, etc.\n functions: Set<string> // Do, DoAsync, $, etc.\n all: Set<string> // Combined for compatibility\n}\n\ntype SpecifierBuckets = { readonly type: string | null; readonly fn: string | null; readonly all: string }\n\n/** Sort a single import specifier into the three buckets (or none). */\nfunction categorizeSpecifier(spec: ASTNode): SpecifierBuckets | null {\n if (spec.type === \"ImportSpecifier\" && spec.imported.type === \"Identifier\") {\n const name = spec.imported.name\n // Named imports split by whether they're a functype TYPE or anything else.\n // (Do/DoAsync/$ + any other named import bucket as `functions`, matching\n // the original behavior — the explicit Do/DoAsync/$ branch was redundant.)\n return TYPE_NAMES.has(name) ? { type: name, fn: null, all: name } : { type: null, fn: name, all: name }\n }\n if (spec.type === \"ImportDefaultSpecifier\") return { type: null, fn: \"default\", all: \"default\" }\n if (spec.type === \"ImportNamespaceSpecifier\") return { type: \"*\", fn: \"*\", all: \"*\" }\n return null\n}\n\nconst pickPresent = <K extends keyof SpecifierBuckets>(buckets: readonly SpecifierBuckets[], key: K): string[] =>\n buckets.flatMap((b) => {\n const v = b[key]\n return v === null ? [] : [v as string]\n })\n\n/**\n * Get imported functype symbols from the current file\n */\nexport function getFunctypeImports(context: Rule.RuleContext): FunctypeImports {\n const buckets = context.sourceCode.ast.body\n .filter(isFunctypeImport)\n .flatMap((node: ASTNode) => (node.specifiers ?? []) as ASTNode[])\n .map(categorizeSpecifier)\n .filter((b): b is SpecifierBuckets => b !== null)\n\n return {\n types: new Set(pickPresent(buckets, \"type\")),\n functions: new Set(pickPresent(buckets, \"fn\")),\n all: new Set(buckets.map((b) => b.all)),\n }\n}\n\n/**\n * Legacy compatibility - returns the 'all' set\n */\nexport function getFunctypeImportsLegacy(context: Rule.RuleContext): Set<string> {\n return getFunctypeImports(context).all\n}\n\n/**\n * Check if a type reference is using functype types\n */\nexport function isFunctypeType(node: ASTNode, functypeImports: FunctypeImports | Set<string>): boolean {\n if (!node) return false\n\n // Handle both new and legacy API\n const types = functypeImports instanceof Set ? functypeImports : functypeImports.types || functypeImports.all\n\n // Check direct type names\n if (node.type === \"TSTypeReference\" && node.typeName?.type === \"Identifier\") {\n const typeName = node.typeName.name\n return (\n types.has(typeName) ||\n [\"Option\", \"Either\", \"List\", \"LazyList\", \"Task\", \"Try\", \"Map\", \"Set\", \"Stack\"].includes(typeName)\n )\n }\n\n return false\n}\n\n/**\n * Check if a call expression is using functype methods\n */\nexport function isFunctypeCall(node: ASTNode, functypeImports: Set<string>): boolean {\n if (!node || node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n\n // Check for static method calls like Option.some(), Either.left(), List.of()\n if (callee.type === \"MemberExpression\" && callee.object.type === \"Identifier\") {\n const objectName = callee.object.name\n const methodName = callee.property?.name\n\n // Check if calling methods on imported functype types\n if (functypeImports.has(objectName)) return true\n\n // Check for common functype patterns\n if (\n (objectName === \"Option\" && [\"some\", \"none\", \"of\"].includes(methodName)) ||\n (objectName === \"Either\" && [\"left\", \"right\", \"of\"].includes(methodName)) ||\n (objectName === \"List\" && [\"of\", \"from\", \"empty\"].includes(methodName)) ||\n (objectName === \"Map\" && [\"of\", \"empty\"].includes(methodName)) ||\n (objectName === \"Set\" && [\"of\", \"empty\"].includes(methodName))\n ) {\n return true\n }\n }\n\n // Check for method calls on functype instances like someOption.map()\n if (callee.type === \"MemberExpression\") {\n const methodName = callee.property?.name\n\n // Common functype methods\n if (\n [\n \"map\",\n \"flatMap\",\n \"filter\",\n \"fold\",\n \"foldLeft\",\n \"foldRight\",\n \"getOrElse\",\n \"orElse\",\n \"isEmpty\",\n \"nonEmpty\",\n \"isDefined\",\n \"isSome\",\n \"isNone\",\n \"isLeft\",\n \"isRight\",\n \"toArray\",\n ].includes(methodName)\n ) {\n return true\n }\n }\n\n return false\n}\n\n/** True iff any ancestor of `node` satisfies `pred`. Pure tail recursion. */\nfunction ancestorSatisfies(node: ASTNode | null | undefined, pred: (n: ASTNode) => boolean): boolean {\n const parent = node?.parent as ASTNode | undefined\n if (!parent) return false\n return pred(parent) || ancestorSatisfies(parent, pred)\n}\n\n/**\n * Check if current context is already using functype patterns appropriately\n */\nexport function isAlreadyUsingFunctype(node: ASTNode, functypeImports: Set<string>): boolean {\n return ancestorSatisfies(\n node,\n (parent) => isFunctypeCall(parent, functypeImports) || isFunctypeType(parent, functypeImports),\n )\n}\n\n/**\n * Check if a variable or parameter is typed with functype types\n */\nexport function hasFunctypeTypeAnnotation(node: ASTNode, functypeImports: FunctypeImports | Set<string>): boolean {\n // Check for type annotation\n if (node.typeAnnotation?.typeAnnotation) {\n return isFunctypeType(node.typeAnnotation.typeAnnotation, functypeImports)\n }\n\n return false\n}\n\n/**\n * Check if a call expression is a Do notation call\n */\nexport function isDoNotationCall(node: ASTNode, functypeImports: FunctypeImports): boolean {\n if (!node || node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n\n // Check for Do() or DoAsync() calls\n if (callee.type === \"Identifier\") {\n const name = callee.name\n return functypeImports.functions.has(name) && [\"Do\", \"DoAsync\"].includes(name)\n }\n\n return false\n}\n\n/**\n * Check if a call expression uses the $ helper function\n */\nexport function isDollarHelper(node: ASTNode, functypeImports: FunctypeImports): boolean {\n if (!node || node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n\n // Check for $() calls\n if (callee.type === \"Identifier\" && callee.name === \"$\") {\n return functypeImports.functions.has(\"$\")\n }\n\n return false\n}\n\n/**\n * Check if current expression is inside a Do notation block\n */\nexport function isInsideDoNotation(node: ASTNode, functypeImports: FunctypeImports): boolean {\n return ancestorSatisfies(node, (n) => n.type === \"CallExpression\" && isDoNotationCall(n, functypeImports))\n}\n\n/**\n * Check if a method call is a chained method call on functype types\n */\nexport function isChainedMethodCall(node: ASTNode, methodName: string): boolean {\n if (!node || node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n\n if (\n callee.type === \"MemberExpression\" &&\n callee.property.type === \"Identifier\" &&\n callee.property.name === methodName\n ) {\n return true\n }\n\n return false\n}\n\n/**\n * Detect if code could benefit from Do notation\n */\nexport function shouldUseDoNotation(\n node: ASTNode,\n functypeImports: FunctypeImports,\n): {\n shouldUse: boolean\n reason: \"nested-checks\" | \"chained-flatmaps\" | \"mixed-monads\" | \"async-chains\" | null\n} {\n // Already using Do notation\n if (isInsideDoNotation(node, functypeImports)) {\n return { shouldUse: false, reason: null }\n }\n\n // Check for nested null checks (a && a.b && a.b.c pattern)\n if (node.type === \"LogicalExpression\" && node.operator === \"&&\") {\n const text = node.toString ? node.toString() : \"\"\n if (text.includes(\"&&\") && text.match(/\\w+(\\.\\w+){2,}/)) {\n return { shouldUse: true, reason: \"nested-checks\" }\n }\n }\n\n // Check for chained flatMap calls\n if (isChainedMethodCall(node, \"flatMap\") && flatMapChainDepth(node) >= 3) {\n return { shouldUse: true, reason: \"chained-flatmaps\" }\n }\n\n return { shouldUse: false, reason: null }\n}\n\n/** Length of an unbroken `.flatMap().flatMap()...` chain starting at `node`. */\nfunction flatMapChainDepth(node: ASTNode): number {\n if (!node || node.type !== \"CallExpression\" || !isChainedMethodCall(node, \"flatMap\")) return 0\n if (node.callee.type !== \"MemberExpression\") return 1\n return 1 + flatMapChainDepth(node.callee.object as ASTNode)\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- function e(e,t){let n=e.ast;for(let e of n.body)if(e.type===`ImportDeclaration`&&e.source.type===`Literal`&&e.source.value===`functype`&&e.specifiers){for(let n of e.specifiers)if(n.type===`ImportNamespaceSpecifier`||n.type===`ImportSpecifier`&&n.imported.type===`Identifier`&&n.imported.name===t)return!0}return!1}function t(t,n){return r=>{let i=t.ast,a=null,o=null;for(let e of i.body)e.type===`ImportDeclaration`&&(o=e,e.source.type===`Literal`&&e.source.value===`functype`&&(a=e));if(e(t,n))return null;if(a&&a.type===`ImportDeclaration`){let e=a;if(e.specifiers?.some(e=>e.type===`ImportNamespaceSpecifier`)??!1)return null;let i=(e.specifiers??[]).filter(e=>e.type===`ImportSpecifier`);if(i.length>0){let e=i[i.length-1];if(e)return r.insertTextAfter(e,`, ${n}`)}let o=t.getText(e).replace(/\{(\s*)\}/,`{ ${n} }`);return r.replaceText(e,o)}let s=`import { ${n} } from "functype"`;if(o)return r.insertTextAfter(o,`\n${s}`);let c=i.body[0];return c?r.insertTextBefore(c,`${s}\n`):r.insertTextBeforeRange([0,0],`${s}\n`)}}export{t as createImportFixer,e as hasFunctypeSymbol};
1
+ const e=e=>e.type===`ImportDeclaration`,t=t=>e(t)&&t.source?.type===`Literal`&&t.source.value===`functype`,n=(e,t)=>e.type===`ImportNamespaceSpecifier`||e.type===`ImportSpecifier`&&e.imported?.type===`Identifier`&&e.imported.name===t;function r(e,r){return e.ast.body.filter(e=>t(e)).flatMap(e=>e.specifiers??[]).some(e=>n(e,r))}function i(n,i){return a=>{if(r(n,i))return null;let o=n.ast,s=o.body.filter(t=>e(t)),c=s.at(-1)??null,l=s.find(e=>t(e))??null;if(l){if(l.specifiers?.some(e=>e.type===`ImportNamespaceSpecifier`)??!1)return null;let e=(l.specifiers??[]).filter(e=>e.type===`ImportSpecifier`).at(-1);if(e)return a.insertTextAfter(e,`, ${i}`);let t=n.getText(l);return a.replaceText(l,t.replace(/\{(\s*)\}/,`{ ${i} }`))}let u=`import { ${i} } from "functype"`;if(c)return a.insertTextAfter(c,`\n${u}`);let d=o.body[0];return d?a.insertTextBefore(d,`${u}\n`):a.insertTextBeforeRange([0,0],`${u}\n`)}}export{i as createImportFixer,r as hasFunctypeSymbol};
2
2
  //# sourceMappingURL=import-fixer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"import-fixer.js","names":[],"sources":["../../src/utils/import-fixer.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\nimport type { SourceCode } from \"eslint\"\n\n/**\n * Check if a given symbol is already imported from functype\n */\nexport function hasFunctypeSymbol(sourceCode: SourceCode, symbolName: string): boolean {\n const program = sourceCode.ast\n\n for (const node of program.body) {\n if (node.type === \"ImportDeclaration\" && node.source.type === \"Literal\" && node.source.value === \"functype\") {\n if (node.specifiers) {\n for (const spec of node.specifiers) {\n if (spec.type === \"ImportNamespaceSpecifier\") {\n return true\n }\n if (\n spec.type === \"ImportSpecifier\" &&\n spec.imported.type === \"Identifier\" &&\n spec.imported.name === symbolName\n ) {\n return true\n }\n }\n }\n }\n }\n\n return false\n}\n\n/**\n * Create a fixer function that adds a symbol to the functype import.\n * Returns a factory compatible with ESLint suggest[].fix signature.\n */\nexport function createImportFixer(\n sourceCode: SourceCode,\n symbolName: string,\n): (fixer: Rule.RuleFixer) => Rule.Fix | null {\n return (fixer: Rule.RuleFixer): Rule.Fix | null => {\n const program = sourceCode.ast\n\n let existingFunctypeImport: (typeof program.body)[number] | null = null\n let lastImportNode: (typeof program.body)[number] | null = null\n\n for (const node of program.body) {\n if (node.type === \"ImportDeclaration\") {\n lastImportNode = node\n if (node.source.type === \"Literal\" && node.source.value === \"functype\") {\n existingFunctypeImport = node\n }\n }\n }\n\n // If already imported, nothing to do\n if (hasFunctypeSymbol(sourceCode, symbolName)) {\n return null\n }\n\n if (existingFunctypeImport && existingFunctypeImport.type === \"ImportDeclaration\") {\n const importDecl = existingFunctypeImport\n\n // Check for namespace import — can't add named imports alongside it\n const hasNamespace = importDecl.specifiers?.some((s) => s.type === \"ImportNamespaceSpecifier\") ?? false\n if (hasNamespace) {\n return null\n }\n\n // Find the last named specifier and append after it\n const specifiers = importDecl.specifiers ?? []\n const namedSpecifiers = specifiers.filter((s) => s.type === \"ImportSpecifier\")\n\n if (namedSpecifiers.length > 0) {\n const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1]\n if (lastSpecifier) {\n return fixer.insertTextAfter(lastSpecifier, `, ${symbolName}`)\n }\n }\n\n // No named specifiers yet insert before closing brace\n const importText = sourceCode.getText(importDecl)\n const newText = importText.replace(/\\{(\\s*)\\}/, `{ ${symbolName} }`)\n return fixer.replaceText(importDecl, newText)\n }\n\n // No existing functype import — add a new one\n const newImport = `import { ${symbolName} } from \"functype\"`\n\n if (lastImportNode) {\n return fixer.insertTextAfter(lastImportNode, `\\n${newImport}`)\n }\n\n // No imports at all — insert at top of file\n const firstNode = program.body[0]\n if (firstNode) {\n return fixer.insertTextBefore(firstNode, `${newImport}\\n`)\n }\n\n return fixer.insertTextBeforeRange([0, 0], `${newImport}\\n`)\n }\n}\n"],"mappings":"AAMA,SAAgB,EAAkB,EAAwB,EAA6B,CACrF,IAAM,EAAU,EAAW,IAE3B,IAAK,IAAM,KAAQ,EAAQ,KACzB,GAAI,EAAK,OAAS,qBAAuB,EAAK,OAAO,OAAS,WAAa,EAAK,OAAO,QAAU,YAC3F,EAAK,WACP,KAAK,IAAM,KAAQ,EAAK,WAItB,GAHI,EAAK,OAAS,4BAIhB,EAAK,OAAS,mBACd,EAAK,SAAS,OAAS,cACvB,EAAK,SAAS,OAAS,EAEvB,MAAO,EAEX,CAKN,MAAO,EACT,CAMA,SAAgB,EACd,EACA,EAC4C,CAC5C,MAAQ,IAA2C,CACjD,IAAM,EAAU,EAAW,IAEvB,EAA+D,KAC/D,EAAuD,KAE3D,IAAK,IAAM,KAAQ,EAAQ,KACrB,EAAK,OAAS,sBAChB,EAAiB,EACb,EAAK,OAAO,OAAS,WAAa,EAAK,OAAO,QAAU,aAC1D,EAAyB,IAM/B,GAAI,EAAkB,EAAY,CAAU,EAC1C,OAAO,KAGT,GAAI,GAA0B,EAAuB,OAAS,oBAAqB,CACjF,IAAM,EAAa,EAInB,GADqB,EAAW,YAAY,KAAM,GAAM,EAAE,OAAS,0BAA0B,GAAK,GAEhG,OAAO,KAKT,IAAM,GADa,EAAW,YAAc,CAAC,EAAA,CACV,OAAQ,GAAM,EAAE,OAAS,iBAAiB,EAE7E,GAAI,EAAgB,OAAS,EAAG,CAC9B,IAAM,EAAgB,EAAgB,EAAgB,OAAS,GAC/D,GAAI,EACF,OAAO,EAAM,gBAAgB,EAAe,KAAK,GAAY,CAEjE,CAIA,IAAM,EADa,EAAW,QAAQ,CACb,CAAC,CAAC,QAAQ,YAAa,KAAK,EAAW,GAAG,EACnE,OAAO,EAAM,YAAY,EAAY,CAAO,CAC9C,CAGA,IAAM,EAAY,YAAY,EAAW,oBAEzC,GAAI,EACF,OAAO,EAAM,gBAAgB,EAAgB,KAAK,GAAW,EAI/D,IAAM,EAAY,EAAQ,KAAK,GAK/B,OAJI,EACK,EAAM,iBAAiB,EAAW,GAAG,EAAU,GAAG,EAGpD,EAAM,sBAAsB,CAAC,EAAG,CAAC,EAAG,GAAG,EAAU,GAAG,CAC7D,CACF"}
1
+ {"version":3,"file":"import-fixer.js","names":[],"sources":["../../src/utils/import-fixer.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\nimport type { SourceCode } from \"eslint\"\n\ntype ImportSpec = {\n type: string\n imported?: { type: string; name: string }\n}\ntype ImportNode = {\n type: \"ImportDeclaration\"\n source: { type: string; value: string }\n specifiers?: readonly ImportSpec[]\n}\n\nconst isImportDecl = (node: { type: string }): node is ImportNode => node.type === \"ImportDeclaration\"\n\nconst isFunctypeImportDecl = (node: { type: string }): node is ImportNode =>\n isImportDecl(node) && node.source?.type === \"Literal\" && node.source.value === \"functype\"\n\nconst specifierMatches = (spec: ImportSpec, symbolName: string): boolean =>\n spec.type === \"ImportNamespaceSpecifier\" ||\n (spec.type === \"ImportSpecifier\" && spec.imported?.type === \"Identifier\" && spec.imported.name === symbolName)\n\n/**\n * Check if a given symbol is already imported from functype\n */\nexport function hasFunctypeSymbol(sourceCode: SourceCode, symbolName: string): boolean {\n return (sourceCode.ast.body as readonly unknown[])\n .filter((n): n is ImportNode => isFunctypeImportDecl(n as { type: string }))\n .flatMap((node) => node.specifiers ?? [])\n .some((spec) => specifierMatches(spec, symbolName))\n}\n\n/**\n * Create a fixer function that adds a symbol to the functype import.\n * Returns a factory compatible with ESLint suggest[].fix signature.\n */\nexport function createImportFixer(\n sourceCode: SourceCode,\n symbolName: string,\n): (fixer: Rule.RuleFixer) => Rule.Fix | null {\n return (fixer: Rule.RuleFixer): Rule.Fix | null => {\n // Already imported — nothing to do.\n if (hasFunctypeSymbol(sourceCode, symbolName)) return null\n\n const program = sourceCode.ast\n const imports = (program.body as readonly unknown[]).filter((n): n is ImportNode =>\n isImportDecl(n as { type: string }),\n )\n const lastImportNode = imports.at(-1) ?? null\n const existingFunctypeImport = imports.find((n) => isFunctypeImportDecl(n)) ?? null\n\n if (existingFunctypeImport) {\n // Namespace import (`import * as F from \"functype\"`) — can't add named.\n const hasNamespace =\n existingFunctypeImport.specifiers?.some((s) => s.type === \"ImportNamespaceSpecifier\") ?? false\n if (hasNamespace) return null\n\n // Append after the last named specifier if one exists.\n const namedSpecifiers = (existingFunctypeImport.specifiers ?? []).filter((s) => s.type === \"ImportSpecifier\")\n const lastSpecifier = namedSpecifiers.at(-1)\n if (lastSpecifier) return fixer.insertTextAfter(lastSpecifier as never, `, ${symbolName}`)\n\n // No named specifiers — fill the empty braces.\n const importText = sourceCode.getText(existingFunctypeImport as never)\n return fixer.replaceText(existingFunctypeImport as never, importText.replace(/\\{(\\s*)\\}/, `{ ${symbolName} }`))\n }\n\n // No existing functype import — insert a new one.\n const newImport = `import { ${symbolName} } from \"functype\"`\n if (lastImportNode) return fixer.insertTextAfter(lastImportNode as never, `\\n${newImport}`)\n\n const firstNode = program.body[0]\n if (firstNode) return fixer.insertTextBefore(firstNode, `${newImport}\\n`)\n return fixer.insertTextBeforeRange([0, 0], `${newImport}\\n`)\n }\n}\n"],"mappings":"AAaA,MAAM,EAAgB,GAA+C,EAAK,OAAS,oBAE7E,EAAwB,GAC5B,EAAa,CAAI,GAAK,EAAK,QAAQ,OAAS,WAAa,EAAK,OAAO,QAAU,WAE3E,GAAoB,EAAkB,IAC1C,EAAK,OAAS,4BACb,EAAK,OAAS,mBAAqB,EAAK,UAAU,OAAS,cAAgB,EAAK,SAAS,OAAS,EAKrG,SAAgB,EAAkB,EAAwB,EAA6B,CACrF,OAAQ,EAAW,IAAI,KACpB,OAAQ,GAAuB,EAAqB,CAAqB,CAAC,CAAC,CAC3E,QAAS,GAAS,EAAK,YAAc,CAAC,CAAC,CAAC,CACxC,KAAM,GAAS,EAAiB,EAAM,CAAU,CAAC,CACtD,CAMA,SAAgB,EACd,EACA,EAC4C,CAC5C,MAAQ,IAA2C,CAEjD,GAAI,EAAkB,EAAY,CAAU,EAAG,OAAO,KAEtD,IAAM,EAAU,EAAW,IACrB,EAAW,EAAQ,KAA4B,OAAQ,GAC3D,EAAa,CAAqB,CACpC,EACM,EAAiB,EAAQ,GAAG,EAAE,GAAK,KACnC,EAAyB,EAAQ,KAAM,GAAM,EAAqB,CAAC,CAAC,GAAK,KAE/E,GAAI,EAAwB,CAI1B,GADE,EAAuB,YAAY,KAAM,GAAM,EAAE,OAAS,0BAA0B,GAAK,GACzE,OAAO,KAIzB,IAAM,GADmB,EAAuB,YAAc,CAAC,EAAA,CAAG,OAAQ,GAAM,EAAE,OAAS,iBACvD,CAAC,CAAC,GAAG,EAAE,EAC3C,GAAI,EAAe,OAAO,EAAM,gBAAgB,EAAwB,KAAK,GAAY,EAGzF,IAAM,EAAa,EAAW,QAAQ,CAA+B,EACrE,OAAO,EAAM,YAAY,EAAiC,EAAW,QAAQ,YAAa,KAAK,EAAW,GAAG,CAAC,CAChH,CAGA,IAAM,EAAY,YAAY,EAAW,oBACzC,GAAI,EAAgB,OAAO,EAAM,gBAAgB,EAAyB,KAAK,GAAW,EAE1F,IAAM,EAAY,EAAQ,KAAK,GAE/B,OADI,EAAkB,EAAM,iBAAiB,EAAW,GAAG,EAAU,GAAG,EACjE,EAAM,sBAAsB,CAAC,EAAG,CAAC,EAAG,GAAG,EAAU,GAAG,CAC7D,CACF"}