eslint-plugin-functype 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dependency-validator-BBxa9-7D.js.map +1 -1
- package/dist/rules/no-let.js.map +1 -1
- package/dist/rules/prefer-do-notation.js.map +1 -1
- package/dist/rules/prefer-either.js.map +1 -1
- package/dist/rules/prefer-flatmap.js.map +1 -1
- package/dist/rules/prefer-functype-map.js.map +1 -1
- package/dist/rules/prefer-functype-set.js.map +1 -1
- package/dist/rules/prefer-list.js.map +1 -1
- package/dist/rules/prefer-map.js.map +1 -1
- package/dist/rules/prefer-option.js.map +1 -1
- package/dist/utils/import-fixer.js.map +1 -1
- package/package.json +29 -27
- package/LICENSE +0 -21
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dependency-validator-BBxa9-7D.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 const missing: PeerDependency[] = []\n const available: PeerDependency[] = []\n const warnings: string[] = []\n\n for (const dep of PEER_DEPENDENCIES) {\n if (tryRequire(dep.packageName)) {\n available.push(dep)\n } else {\n missing.push(dep)\n if (dep.required) {\n // Required dependency is missing - this will cause errors\n } else {\n // Optional dependency is missing - add warning\n warnings.push(`Optional plugin '${dep.name}' not found. Some rules will be skipped.`)\n }\n }\n }\n\n const requiredMissing = missing.filter((dep) => dep.required)\n const isValid = requiredMissing.length === 0\n\n // Generate install command for missing dependencies\n const missingPackageNames = missing.map((dep) => dep.packageName)\n const installCommand = missingPackageNames.length > 0 ? `pnpm add -D ${missingPackageNames.join(\" \")}` : \"\"\n\n return {\n isValid,\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,GACX,CACD,CACE,KAAM,4BACN,YAAa,4BACb,YAAa,+BACb,SAAU,GACX,CACD,CACE,KAAM,2BACN,YAAa,2BACb,YAAa,sCACb,SAAU,GACX,CACD,CACE,KAAM,yBACN,YAAa,yBACb,YAAa,wBACb,SAAU,GACX,CACD,CACE,KAAM,mCACN,YAAa,mCACb,YAAa,uBACb,SAAU,GACX,CACD,CACE,KAAM,WACN,YAAa,WACb,YAAa,iBACb,SAAU,GACX,CACF,CAUD,SAAS,EAAW,EAA8B,CAChD,GAAI,CAEF,OADA,EAAQ,QAAQ,EAAY,CACrB,QACD,CACN,MAAO,IAIX,SAAgB,GAA6C,CAC3D,IAAM,EAA4B,EAAE,CAC9B,EAA8B,EAAE,CAChC,EAAqB,EAAE,CAE7B,IAAK,IAAM,KAAO,EACZ,EAAW,EAAI,YAAY,CAC7B,EAAU,KAAK,EAAI,EAEnB,EAAQ,KAAK,EAAI,CACb,EAAI,UAIN,EAAS,KAAK,oBAAoB,EAAI,KAAK,0CAA0C,EAM3F,IAAM,EADkB,EAAQ,OAAQ,GAAQ,EAAI,
|
|
1
|
+
{"version":3,"file":"dependency-validator-BBxa9-7D.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 const missing: PeerDependency[] = []\n const available: PeerDependency[] = []\n const warnings: string[] = []\n\n for (const dep of PEER_DEPENDENCIES) {\n if (tryRequire(dep.packageName)) {\n available.push(dep)\n } else {\n missing.push(dep)\n if (dep.required) {\n // Required dependency is missing - this will cause errors\n } else {\n // Optional dependency is missing - add warning\n warnings.push(`Optional plugin '${dep.name}' not found. Some rules will be skipped.`)\n }\n }\n }\n\n const requiredMissing = missing.filter((dep) => dep.required)\n const isValid = requiredMissing.length === 0\n\n // Generate install command for missing dependencies\n const missingPackageNames = missing.map((dep) => dep.packageName)\n const installCommand = missingPackageNames.length > 0 ? `pnpm add -D ${missingPackageNames.join(\" \")}` : \"\"\n\n return {\n isValid,\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,GACX,CACD,CACE,KAAM,4BACN,YAAa,4BACb,YAAa,+BACb,SAAU,GACX,CACD,CACE,KAAM,2BACN,YAAa,2BACb,YAAa,sCACb,SAAU,GACX,CACD,CACE,KAAM,yBACN,YAAa,yBACb,YAAa,wBACb,SAAU,GACX,CACD,CACE,KAAM,mCACN,YAAa,mCACb,YAAa,uBACb,SAAU,GACX,CACD,CACE,KAAM,WACN,YAAa,WACb,YAAa,iBACb,SAAU,GACX,CACF,CAUD,SAAS,EAAW,EAA8B,CAChD,GAAI,CAEF,OADA,EAAQ,QAAQ,EAAY,CACrB,QACD,CACN,MAAO,IAIX,SAAgB,GAA6C,CAC3D,IAAM,EAA4B,EAAE,CAC9B,EAA8B,EAAE,CAChC,EAAqB,EAAE,CAE7B,IAAK,IAAM,KAAO,EACZ,EAAW,EAAI,YAAY,CAC7B,EAAU,KAAK,EAAI,EAEnB,EAAQ,KAAK,EAAI,CACb,EAAI,UAIN,EAAS,KAAK,oBAAoB,EAAI,KAAK,0CAA0C,EAM3F,IAAM,EADkB,EAAQ,OAAQ,GAAQ,EAAI,SACrB,CAAC,SAAW,EAGrC,EAAsB,EAAQ,IAAK,GAAQ,EAAI,YAAY,CAGjE,MAAO,CACL,UACA,UACA,YACA,eANqB,EAAoB,OAAS,EAAI,eAAe,EAAoB,KAAK,IAAI,GAAK,GAOvG,WACD,CAGH,SAAgB,EAAsB,EAAiC,CACrE,IAAM,EAAkB,EAAO,QAAQ,OAAQ,GAAQ,EAAI,SAAS,CAEpE,GAAI,EAAgB,SAAW,EAC7B,OAAW,MAAM,uBAAuB,CAK1C,IAAM,EAAU,CACd,mEACA,GAJkB,EAAgB,IAAK,GAAQ,OAAO,EAAI,KAAK,KAAK,EAAI,cAAc,CAAC,KAAK;EAKjF,CACX,GACA,mCACA,MAAM,EAAO,iBACb,GACA,gGACD,CAAC,KAAK;EAAK,CAEZ,OAAW,MAAM,EAAQ,CAG3B,SAAgB,GAAsC,CAEpD,OAAO,QAAQ,IAAI,WAAa,QAAU,QAAQ,IAAI,2BAA6B"}
|
package/dist/rules/no-let.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-let.js","names":[],"sources":["../../src/rules/no-let.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 fixable: \"code\",\n hasSuggestions: true,\n docs: {\n description: \"Prefer const over let — use immutable bindings\",\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 noLet: \"Prefer const over let — use immutable bindings\",\n suggestConst: \"Replace let with const\",\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 return {\n VariableDeclaration(node: ASTNode) {\n if (node.kind !== \"let\") return\n if (allowInTests && isInTestFile()) return\n\n const declaredVars = context.sourceCode.getDeclaredVariables(node)\n const hasReassignment = declaredVars.some((variable: ASTNode) =>\n variable.references.some((ref: ASTNode) => ref.isWrite() && ref.identifier !== variable.defs[0]?.name),\n )\n\n if (!hasReassignment) {\n // Safe to autofix — no reassignment detected\n context.report({\n node,\n messageId: \"noLet\",\n fix(fixer: Rule.RuleFixer) {\n const sourceCode = context.sourceCode\n const firstToken = sourceCode.getFirstToken(node)\n if (firstToken && firstToken.value === \"let\") {\n return fixer.replaceText(firstToken, \"const\")\n }\n return null\n },\n })\n } else {\n // Reassignment found — warn only, with suggestion\n context.report({\n node,\n messageId: \"noLet\",\n suggest: [\n {\n messageId: \"suggestConst\",\n fix(fixer: Rule.RuleFixer) {\n const sourceCode = context.sourceCode\n const firstToken = sourceCode.getFirstToken(node)\n if (firstToken && firstToken.value === \"let\") {\n return fixer.replaceText(firstToken, \"const\")\n }\n return null\n },\n },\n ],\n })\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"AAIA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,QAAS,OACT,eAAgB,GAChB,KAAM,CACJ,YAAa,iDACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,aAAc,CACZ,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,MAAO,iDACP,aAAc,yBACf,CACF,CAED,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,EAAE,EACX,eAAiB,GAE9C,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,EAAS,EAChD,EAAS,SAAS,YAAY,EAC9B,EAAS,SAAS,SAAS,EAC3B,EAAS,SAAS,UAAU,CAIhC,MAAO,CACL,oBAAoB,EAAe,CAC7B,EAAK,OAAS,QACd,GAAgB,GAAc,GAEb,EAAQ,WAAW,qBAAqB,
|
|
1
|
+
{"version":3,"file":"no-let.js","names":[],"sources":["../../src/rules/no-let.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 fixable: \"code\",\n hasSuggestions: true,\n docs: {\n description: \"Prefer const over let — use immutable bindings\",\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 noLet: \"Prefer const over let — use immutable bindings\",\n suggestConst: \"Replace let with const\",\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 return {\n VariableDeclaration(node: ASTNode) {\n if (node.kind !== \"let\") return\n if (allowInTests && isInTestFile()) return\n\n const declaredVars = context.sourceCode.getDeclaredVariables(node)\n const hasReassignment = declaredVars.some((variable: ASTNode) =>\n variable.references.some((ref: ASTNode) => ref.isWrite() && ref.identifier !== variable.defs[0]?.name),\n )\n\n if (!hasReassignment) {\n // Safe to autofix — no reassignment detected\n context.report({\n node,\n messageId: \"noLet\",\n fix(fixer: Rule.RuleFixer) {\n const sourceCode = context.sourceCode\n const firstToken = sourceCode.getFirstToken(node)\n if (firstToken && firstToken.value === \"let\") {\n return fixer.replaceText(firstToken, \"const\")\n }\n return null\n },\n })\n } else {\n // Reassignment found — warn only, with suggestion\n context.report({\n node,\n messageId: \"noLet\",\n suggest: [\n {\n messageId: \"suggestConst\",\n fix(fixer: Rule.RuleFixer) {\n const sourceCode = context.sourceCode\n const firstToken = sourceCode.getFirstToken(node)\n if (firstToken && firstToken.value === \"let\") {\n return fixer.replaceText(firstToken, \"const\")\n }\n return null\n },\n },\n ],\n })\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"AAIA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,QAAS,OACT,eAAgB,GAChB,KAAM,CACJ,YAAa,iDACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,aAAc,CACZ,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,MAAO,iDACP,aAAc,yBACf,CACF,CAED,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,EAAE,EACX,eAAiB,GAE9C,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,EAAS,EAChD,EAAS,SAAS,YAAY,EAC9B,EAAS,SAAS,SAAS,EAC3B,EAAS,SAAS,UAAU,CAIhC,MAAO,CACL,oBAAoB,EAAe,CAC7B,EAAK,OAAS,QACd,GAAgB,GAAc,GAEb,EAAQ,WAAW,qBAAqB,EACzB,CAAC,KAAM,GACzC,EAAS,WAAW,KAAM,GAAiB,EAAI,SAAS,EAAI,EAAI,aAAe,EAAS,KAAK,IAAI,KAAK,CAGpF,CAgBlB,EAAQ,OAAO,CACb,OACA,UAAW,QACX,QAAS,CACP,CACE,UAAW,eACX,IAAI,EAAuB,CAEzB,IAAM,EADa,EAAQ,WACG,cAAc,EAAK,CAIjD,OAHI,GAAc,EAAW,QAAU,MAC9B,EAAM,YAAY,EAAY,QAAQ,CAExC,MAEV,CACF,CACF,CAAC,CA9BF,EAAQ,OAAO,CACb,OACA,UAAW,QACX,IAAI,EAAuB,CAEzB,IAAM,EADa,EAAQ,WACG,cAAc,EAAK,CAIjD,OAHI,GAAc,EAAW,QAAU,MAC9B,EAAM,YAAY,EAAY,QAAQ,CAExC,MAEV,CAAC,IAsBP,EAEJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-do-notation.js","names":[],"sources":["../../src/rules/prefer-do-notation.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImports, isChainedMethodCall } from \"../utils/functype-detection\"\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer Do notation for complex monadic compositions and nested operations\",\n recommended: true,\n },\n fixable: \"code\",\n messages: {\n preferDoForNestedChecks: \"Prefer Do notation for nested null/undefined checks instead of logical AND chains\",\n preferDoForChainedMethods: \"Prefer Do notation for complex flatMap chains ({{count}} levels deep)\",\n preferDoForMixedMonads: \"Consider Do notation when mixing Option, Either, and Try operations\",\n preferDoAsyncForChainedTasks: \"Prefer DoAsync notation for chained async operations\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n minChainDepth: {\n type: \"integer\",\n minimum: 2,\n default: 3,\n },\n detectMixedMonads: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n create(context) {\n const options = context.options[0] || {}\n const minChainDepth = options.minChainDepth || 3\n const detectMixedMonads = options.detectMixedMonads !== false\n\n const functypeImports = getFunctypeImports(context)\n\n // Track if Do notation is already imported\n const hasDoImport = functypeImports.functions.has(\"Do\") || functypeImports.functions.has(\"DoAsync\")\n\n /**\n * Detect nested null/undefined checks like: a && a.b && a.b.c\n */\n function checkNestedNullChecks(node: ASTNode): void {\n if (node.operator !== \"&&\") return\n\n let depth = 0\n let current: ASTNode = node\n\n // Count the depth of && chains with property access\n while (current.type === \"LogicalExpression\" && current.operator === \"&&\") {\n if (\n current.right.type === \"MemberExpression\" ||\n (current.right.type === \"LogicalExpression\" && current.right.operator === \"&&\")\n ) {\n depth++\n }\n current = current.left\n }\n\n // Check if this looks like nested property access guarding\n if (depth >= 2 && hasNestedPropertyAccess(node)) {\n context.report({\n node,\n messageId: \"preferDoForNestedChecks\",\n fix: hasDoImport ? (fixer) => fixNestedChecks(fixer, node) : undefined,\n })\n }\n }\n\n /**\n * Check if logical expression contains nested property access patterns\n */\n function hasNestedPropertyAccess(node: ASTNode): boolean {\n const text = context.sourceCode.getText(node)\n\n // Look for patterns like: obj && obj.prop && obj.prop.nested\n const nestedPattern = /\\w+(\\.\\w+){2,}/g\n const matches = text.match(nestedPattern)\n\n return matches !== null && matches.length > 0\n }\n\n /**\n * Detect long flatMap chains\n */\n function checkChainedMethods(node: ASTNode): void {\n if (!isChainedMethodCall(node, \"flatMap\")) return\n\n const chainDepth = getChainDepth(node)\n\n if (chainDepth >= minChainDepth) {\n context.report({\n node,\n messageId: \"preferDoForChainedMethods\",\n data: { count: chainDepth.toString() },\n fix: hasDoImport ? (fixer) => fixChainedMethods(fixer, node) : undefined,\n })\n }\n }\n\n /**\n * Calculate chain depth for method calls\n */\n function getChainDepth(node: ASTNode): number {\n let depth = 1\n let current = node\n\n while (current.callee.type === \"MemberExpression\" && current.callee.object.type === \"CallExpression\") {\n const method = current.callee.property\n if (method.type === \"Identifier\" && [\"flatMap\", \"map\", \"filter\", \"fold\"].includes(method.name)) {\n depth++\n }\n current = current.callee.object\n }\n\n return depth\n }\n\n /**\n * Auto-fix for nested null checks\n */\n function fixNestedChecks(fixer: Rule.RuleFixer, node: ASTNode): Rule.Fix {\n const text = context.sourceCode.getText(node)\n\n // Simple transformation for common patterns\n // a && a.b && a.b.c -> Do(function* () { const x = yield* $(Option(a)); const y = yield* $(Option(x.b)); return Option(y.c) })\n const match = text.match(/(\\w+)(\\.\\w+)+/)\n if (match) {\n const baseVar = match[1]\n const chain = match[0]\n\n const doNotation = `Do(function* () {\n const obj = yield* $(Option(${baseVar}))\n return yield* $(Option(obj${chain.substring(baseVar.length)}))\n})`\n\n return fixer.replaceText(node, doNotation)\n }\n\n return fixer.replaceText(node, `/* TODO: Convert to Do notation */ ${text}`)\n }\n\n /**\n * Auto-fix for chained methods (basic)\n */\n function fixChainedMethods(fixer: Rule.RuleFixer, node: ASTNode): Rule.Fix {\n const text = context.sourceCode.getText(node)\n\n // For now, just add a comment suggesting Do notation\n return fixer.replaceText(node, `/* TODO: Consider Do notation for complex chains */ ${text}`)\n }\n\n /**\n * Detect mixed monad usage\n */\n function checkMixedMonads(node: ASTNode): void {\n if (!detectMixedMonads) return\n\n const text = context.sourceCode.getText(node)\n const monadTypes = [\"Option\", \"Either\", \"Try\", \"Task\"]\n const foundMonads = monadTypes.filter((type) => text.includes(type))\n\n if (foundMonads.length >= 2) {\n context.report({\n node,\n messageId: \"preferDoForMixedMonads\",\n })\n }\n }\n\n return {\n LogicalExpression(node) {\n checkNestedNullChecks(node)\n },\n\n CallExpression(node) {\n checkChainedMethods(node)\n checkMixedMonads(node)\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"6FAKA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,KAAM,CACJ,YAAa,4EACb,YAAa,GACd,CACD,QAAS,OACT,SAAU,CACR,wBAAyB,oFACzB,0BAA2B,wEAC3B,uBAAwB,sEACxB,6BAA8B,uDAC/B,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,cAAe,CACb,KAAM,UACN,QAAS,EACT,QAAS,EACV,CACD,kBAAmB,CACjB,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACF,CACD,OAAO,EAAS,CACd,IAAM,EAAU,EAAQ,QAAQ,IAAM,EAAE,CAClC,EAAgB,EAAQ,eAAiB,EACzC,EAAoB,EAAQ,oBAAsB,GAElD,EAAkB,EAAmB,EAAQ,CAG7C,EAAc,EAAgB,UAAU,IAAI,KAAK,EAAI,EAAgB,UAAU,IAAI,UAAU,CAKnG,SAAS,EAAsB,EAAqB,CAClD,GAAI,EAAK,WAAa,KAAM,OAE5B,IAAI,EAAQ,EACR,EAAmB,EAGvB,KAAO,EAAQ,OAAS,qBAAuB,EAAQ,WAAa,OAEhE,EAAQ,MAAM,OAAS,oBACtB,EAAQ,MAAM,OAAS,qBAAuB,EAAQ,MAAM,WAAa,OAE1E,IAEF,EAAU,EAAQ,KAIhB,GAAS,GAAK,EAAwB,EAAK,EAC7C,EAAQ,OAAO,CACb,OACA,UAAW,0BACX,IAAK,EAAe,GAAU,EAAgB,EAAO,EAAK,CAAG,IAAA,GAC9D,CAAC,CAON,SAAS,EAAwB,EAAwB,CAKvD,IAAM,EAJO,EAAQ,WAAW,QAAQ,
|
|
1
|
+
{"version":3,"file":"prefer-do-notation.js","names":[],"sources":["../../src/rules/prefer-do-notation.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImports, isChainedMethodCall } from \"../utils/functype-detection\"\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer Do notation for complex monadic compositions and nested operations\",\n recommended: true,\n },\n fixable: \"code\",\n messages: {\n preferDoForNestedChecks: \"Prefer Do notation for nested null/undefined checks instead of logical AND chains\",\n preferDoForChainedMethods: \"Prefer Do notation for complex flatMap chains ({{count}} levels deep)\",\n preferDoForMixedMonads: \"Consider Do notation when mixing Option, Either, and Try operations\",\n preferDoAsyncForChainedTasks: \"Prefer DoAsync notation for chained async operations\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n minChainDepth: {\n type: \"integer\",\n minimum: 2,\n default: 3,\n },\n detectMixedMonads: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n create(context) {\n const options = context.options[0] || {}\n const minChainDepth = options.minChainDepth || 3\n const detectMixedMonads = options.detectMixedMonads !== false\n\n const functypeImports = getFunctypeImports(context)\n\n // Track if Do notation is already imported\n const hasDoImport = functypeImports.functions.has(\"Do\") || functypeImports.functions.has(\"DoAsync\")\n\n /**\n * Detect nested null/undefined checks like: a && a.b && a.b.c\n */\n function checkNestedNullChecks(node: ASTNode): void {\n if (node.operator !== \"&&\") return\n\n let depth = 0\n let current: ASTNode = node\n\n // Count the depth of && chains with property access\n while (current.type === \"LogicalExpression\" && current.operator === \"&&\") {\n if (\n current.right.type === \"MemberExpression\" ||\n (current.right.type === \"LogicalExpression\" && current.right.operator === \"&&\")\n ) {\n depth++\n }\n current = current.left\n }\n\n // Check if this looks like nested property access guarding\n if (depth >= 2 && hasNestedPropertyAccess(node)) {\n context.report({\n node,\n messageId: \"preferDoForNestedChecks\",\n fix: hasDoImport ? (fixer) => fixNestedChecks(fixer, node) : undefined,\n })\n }\n }\n\n /**\n * Check if logical expression contains nested property access patterns\n */\n function hasNestedPropertyAccess(node: ASTNode): boolean {\n const text = context.sourceCode.getText(node)\n\n // Look for patterns like: obj && obj.prop && obj.prop.nested\n const nestedPattern = /\\w+(\\.\\w+){2,}/g\n const matches = text.match(nestedPattern)\n\n return matches !== null && matches.length > 0\n }\n\n /**\n * Detect long flatMap chains\n */\n function checkChainedMethods(node: ASTNode): void {\n if (!isChainedMethodCall(node, \"flatMap\")) return\n\n const chainDepth = getChainDepth(node)\n\n if (chainDepth >= minChainDepth) {\n context.report({\n node,\n messageId: \"preferDoForChainedMethods\",\n data: { count: chainDepth.toString() },\n fix: hasDoImport ? (fixer) => fixChainedMethods(fixer, node) : undefined,\n })\n }\n }\n\n /**\n * Calculate chain depth for method calls\n */\n function getChainDepth(node: ASTNode): number {\n let depth = 1\n let current = node\n\n while (current.callee.type === \"MemberExpression\" && current.callee.object.type === \"CallExpression\") {\n const method = current.callee.property\n if (method.type === \"Identifier\" && [\"flatMap\", \"map\", \"filter\", \"fold\"].includes(method.name)) {\n depth++\n }\n current = current.callee.object\n }\n\n return depth\n }\n\n /**\n * Auto-fix for nested null checks\n */\n function fixNestedChecks(fixer: Rule.RuleFixer, node: ASTNode): Rule.Fix {\n const text = context.sourceCode.getText(node)\n\n // Simple transformation for common patterns\n // a && a.b && a.b.c -> Do(function* () { const x = yield* $(Option(a)); const y = yield* $(Option(x.b)); return Option(y.c) })\n const match = text.match(/(\\w+)(\\.\\w+)+/)\n if (match) {\n const baseVar = match[1]\n const chain = match[0]\n\n const doNotation = `Do(function* () {\n const obj = yield* $(Option(${baseVar}))\n return yield* $(Option(obj${chain.substring(baseVar.length)}))\n})`\n\n return fixer.replaceText(node, doNotation)\n }\n\n return fixer.replaceText(node, `/* TODO: Convert to Do notation */ ${text}`)\n }\n\n /**\n * Auto-fix for chained methods (basic)\n */\n function fixChainedMethods(fixer: Rule.RuleFixer, node: ASTNode): Rule.Fix {\n const text = context.sourceCode.getText(node)\n\n // For now, just add a comment suggesting Do notation\n return fixer.replaceText(node, `/* TODO: Consider Do notation for complex chains */ ${text}`)\n }\n\n /**\n * Detect mixed monad usage\n */\n function checkMixedMonads(node: ASTNode): void {\n if (!detectMixedMonads) return\n\n const text = context.sourceCode.getText(node)\n const monadTypes = [\"Option\", \"Either\", \"Try\", \"Task\"]\n const foundMonads = monadTypes.filter((type) => text.includes(type))\n\n if (foundMonads.length >= 2) {\n context.report({\n node,\n messageId: \"preferDoForMixedMonads\",\n })\n }\n }\n\n return {\n LogicalExpression(node) {\n checkNestedNullChecks(node)\n },\n\n CallExpression(node) {\n checkChainedMethods(node)\n checkMixedMonads(node)\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-either.js","names":[],"sources":["../../src/rules/prefer-either.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 Either<E, T> over try/catch blocks and throw statements\",\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 preferEitherOverTryCatch: \"Prefer Either<Error, T> over try/catch block\",\n preferEitherOverThrow: \"Prefer Either.left(error) over throw statement\",\n preferEitherReturn: \"Consider returning Either<Error, {{type}}> instead of throwing\",\n suggestTry: \"Replace with Try(() => ...)\",\n suggestTryFromPromise: \"Replace with Try.fromPromise(...)\",\n suggestEitherLeft: \"Replace with Either.left(...)\",\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 hasThrowStatementsOutsideCatch(node: ASTNode): boolean {\n if (!node) return false\n\n if (node.type === \"ThrowStatement\") {\n // Check if this throw is inside a catch block\n let parent = node.parent\n while (parent) {\n if (parent.type === \"CatchClause\") return false\n parent = parent.parent\n }\n return true\n }\n\n // Skip catch blocks when recursing\n if (node.type === \"CatchClause\") return false\n\n // Recursively check child nodes\n for (const key in node) {\n if (key === \"parent\") continue // Avoid circular references\n const child = node[key]\n if (Array.isArray(child)) {\n for (const item of child) {\n if (item && typeof item === \"object\" && hasThrowStatementsOutsideCatch(item)) {\n return true\n }\n }\n } else if (child && typeof child === \"object\" && hasThrowStatementsOutsideCatch(child)) {\n return true\n }\n }\n\n return false\n }\n\n function checkFunctionForThrows(node: ASTNode): void {\n // Allow functions in test files\n if (allowThrowInTests && isInTestFile()) return\n\n if (!node.body) return\n\n // Only report function-level errors if there are throws NOT in catch blocks\n const hasThrowsNotInCatch = hasThrowStatementsOutsideCatch(node.body)\n if (hasThrowsNotInCatch) {\n const returnType = node.returnType?.typeAnnotation\n if (returnType) {\n const sourceCode = context.sourceCode\n const returnTypeText = sourceCode.getText(returnType)\n\n // Don't report if already using Either\n if (!returnTypeText.includes(\"Either\")) {\n context.report({\n node: node.id || node,\n messageId: \"preferEitherReturn\",\n data: { type: returnTypeText },\n })\n }\n }\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 function isFunctionLike(node: ASTNode): boolean {\n if (!node) return false\n return [\"FunctionDeclaration\", \"FunctionExpression\", \"ArrowFunctionExpression\"].includes(node.type)\n }\n\n function isDirectInFunctionBody(node: ASTNode): boolean {\n const parent = node.parent\n if (!parent) return false\n if (parent.type === \"BlockStatement\" && isFunctionLike(parent.parent)) return true\n if (parent.type === \"BlockStatement\" && parent.parent?.type === \"IfStatement\") {\n const ifParent = parent.parent.parent\n return ifParent?.type === \"BlockStatement\" && isFunctionLike(ifParent.parent)\n }\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: \"preferEitherOverTryCatch\",\n suggest,\n })\n },\n\n ThrowStatement(node: ASTNode) {\n // Allow throws in test files if configured\n if (allowThrowInTests && isInTestFile()) return\n\n // Allow re-throwing in catch blocks (common pattern)\n let parent = node.parent\n while (parent) {\n if (parent.type === \"CatchClause\") return\n parent = parent.parent\n }\n\n const sourceCode = context.sourceCode\n const suggest: Rule.SuggestionReportDescriptor[] = []\n\n if (isDirectInFunctionBody(node)) {\n const throwArg = node.argument\n const argText = sourceCode.getText(throwArg)\n const isErrorExpr =\n throwArg?.type === \"NewExpression\" &&\n throwArg.callee?.type === \"Identifier\" &&\n throwArg.callee.name === \"Error\"\n const eitherArg = isErrorExpr ? argText : `new Error(String(${argText}))`\n\n suggest.push({\n messageId: \"suggestEitherLeft\",\n fix(fixer) {\n return fixer.replaceText(node, `return Either.left(${eitherArg})`)\n },\n })\n\n if (!hasFunctypeSymbol(sourceCode, \"Either\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Either\" },\n fix: createImportFixer(sourceCode, \"Either\"),\n })\n }\n }\n\n context.report({\n node,\n messageId: \"preferEitherOverThrow\",\n suggest,\n })\n },\n\n FunctionDeclaration(node: ASTNode) {\n checkFunctionForThrows(node)\n },\n\n ArrowFunctionExpression(node: ASTNode) {\n checkFunctionForThrows(node)\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prefer-either.js","names":[],"sources":["../../src/rules/prefer-either.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 Either<E, T> over try/catch blocks and throw statements\",\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 preferEitherOverTryCatch: \"Prefer Either<Error, T> over try/catch block\",\n preferEitherOverThrow: \"Prefer Either.left(error) over throw statement\",\n preferEitherReturn: \"Consider returning Either<Error, {{type}}> instead of throwing\",\n suggestTry: \"Replace with Try(() => ...)\",\n suggestTryFromPromise: \"Replace with Try.fromPromise(...)\",\n suggestEitherLeft: \"Replace with Either.left(...)\",\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 hasThrowStatementsOutsideCatch(node: ASTNode): boolean {\n if (!node) return false\n\n if (node.type === \"ThrowStatement\") {\n // Check if this throw is inside a catch block\n let parent = node.parent\n while (parent) {\n if (parent.type === \"CatchClause\") return false\n parent = parent.parent\n }\n return true\n }\n\n // Skip catch blocks when recursing\n if (node.type === \"CatchClause\") return false\n\n // Recursively check child nodes\n for (const key in node) {\n if (key === \"parent\") continue // Avoid circular references\n const child = node[key]\n if (Array.isArray(child)) {\n for (const item of child) {\n if (item && typeof item === \"object\" && hasThrowStatementsOutsideCatch(item)) {\n return true\n }\n }\n } else if (child && typeof child === \"object\" && hasThrowStatementsOutsideCatch(child)) {\n return true\n }\n }\n\n return false\n }\n\n function checkFunctionForThrows(node: ASTNode): void {\n // Allow functions in test files\n if (allowThrowInTests && isInTestFile()) return\n\n if (!node.body) return\n\n // Only report function-level errors if there are throws NOT in catch blocks\n const hasThrowsNotInCatch = hasThrowStatementsOutsideCatch(node.body)\n if (hasThrowsNotInCatch) {\n const returnType = node.returnType?.typeAnnotation\n if (returnType) {\n const sourceCode = context.sourceCode\n const returnTypeText = sourceCode.getText(returnType)\n\n // Don't report if already using Either\n if (!returnTypeText.includes(\"Either\")) {\n context.report({\n node: node.id || node,\n messageId: \"preferEitherReturn\",\n data: { type: returnTypeText },\n })\n }\n }\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 function isFunctionLike(node: ASTNode): boolean {\n if (!node) return false\n return [\"FunctionDeclaration\", \"FunctionExpression\", \"ArrowFunctionExpression\"].includes(node.type)\n }\n\n function isDirectInFunctionBody(node: ASTNode): boolean {\n const parent = node.parent\n if (!parent) return false\n if (parent.type === \"BlockStatement\" && isFunctionLike(parent.parent)) return true\n if (parent.type === \"BlockStatement\" && parent.parent?.type === \"IfStatement\") {\n const ifParent = parent.parent.parent\n return ifParent?.type === \"BlockStatement\" && isFunctionLike(ifParent.parent)\n }\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: \"preferEitherOverTryCatch\",\n suggest,\n })\n },\n\n ThrowStatement(node: ASTNode) {\n // Allow throws in test files if configured\n if (allowThrowInTests && isInTestFile()) return\n\n // Allow re-throwing in catch blocks (common pattern)\n let parent = node.parent\n while (parent) {\n if (parent.type === \"CatchClause\") return\n parent = parent.parent\n }\n\n const sourceCode = context.sourceCode\n const suggest: Rule.SuggestionReportDescriptor[] = []\n\n if (isDirectInFunctionBody(node)) {\n const throwArg = node.argument\n const argText = sourceCode.getText(throwArg)\n const isErrorExpr =\n throwArg?.type === \"NewExpression\" &&\n throwArg.callee?.type === \"Identifier\" &&\n throwArg.callee.name === \"Error\"\n const eitherArg = isErrorExpr ? argText : `new Error(String(${argText}))`\n\n suggest.push({\n messageId: \"suggestEitherLeft\",\n fix(fixer) {\n return fixer.replaceText(node, `return Either.left(${eitherArg})`)\n },\n })\n\n if (!hasFunctypeSymbol(sourceCode, \"Either\")) {\n suggest.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Either\" },\n fix: createImportFixer(sourceCode, \"Either\"),\n })\n }\n }\n\n context.report({\n node,\n messageId: \"preferEitherOverThrow\",\n suggest,\n })\n },\n\n FunctionDeclaration(node: ASTNode) {\n checkFunctionForThrows(node)\n },\n\n ArrowFunctionExpression(node: ASTNode) {\n checkFunctionForThrows(node)\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-flatmap.js","names":[],"sources":["../../src/rules/prefer-flatmap.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 .flatMap() over .map().flat() and nested transformations\",\n recommended: true,\n },\n fixable: \"code\",\n schema: [\n {\n type: \"object\",\n properties: {\n checkNestedMaps: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferFlatMapOverMapFlat: \"Use .flatMap() instead of .map().flat()\",\n preferFlatMapNested: \"Consider .flatMap() for nested transformations that return arrays\",\n preferFlatMapChain: \"Use .flatMap() instead of chained .map() operations that flatten results\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const checkNestedMaps = options.checkNestedMaps !== false\n\n function isMapFollowedByFlat(node: ASTNode): boolean {\n if (node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n if (callee.type !== \"MemberExpression\") return false\n\n // Check if this is .flat()\n if (callee.property.name === \"flat\") {\n const object = callee.object\n\n // Check if the object is a .map() call\n if (\n object.type === \"CallExpression\" &&\n object.callee.type === \"MemberExpression\" &&\n object.callee.property.name === \"map\"\n ) {\n return true\n }\n }\n\n return false\n }\n\n function returnsArray(functionNode: ASTNode): boolean {\n if (!functionNode || !functionNode.body) return false\n\n // Arrow function with expression body\n if (functionNode.body.type === \"ArrayExpression\") {\n return true\n }\n\n // Arrow function with call expression body\n if (functionNode.body.type === \"CallExpression\") {\n const call = functionNode.body\n if (call.callee.type === \"MemberExpression\") {\n const methodName = call.callee.property.name\n // Common methods that return arrays\n if ([\"map\", \"filter\", \"slice\", \"concat\", \"split\"].includes(methodName)) {\n return true\n }\n }\n }\n\n // Function with block body\n if (functionNode.body.type === \"BlockStatement\") {\n const statements = functionNode.body.body\n\n // Look for return statements that return arrays\n for (const stmt of statements) {\n if (stmt.type === \"ReturnStatement\" && stmt.argument) {\n if (stmt.argument.type === \"ArrayExpression\") {\n return true\n }\n\n // Check for method calls that return arrays\n if (stmt.argument.type === \"CallExpression\") {\n const call = stmt.argument\n if (call.callee.type === \"MemberExpression\") {\n const methodName = call.callee.property.name\n // Common methods that return arrays\n if ([\"map\", \"filter\", \"slice\", \"concat\", \"split\"].includes(methodName)) {\n return true\n }\n }\n }\n }\n }\n }\n\n return false\n }\n\n function isNestedMapReturningArrays(node: ASTNode): boolean {\n if (node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n if (callee.type !== \"MemberExpression\") return false\n\n // Check if this is .map()\n if (callee.property.name === \"map\") {\n const callback = node.arguments[0]\n if (callback && (callback.type === \"ArrowFunctionExpression\" || callback.type === \"FunctionExpression\")) {\n return returnsArray(callback)\n }\n }\n\n return false\n }\n\n return {\n CallExpression(node: ASTNode) {\n // Check for .map().flat() pattern first (highest priority)\n if (isMapFollowedByFlat(node)) {\n const sourceCode = context.sourceCode\n\n context.report({\n node,\n messageId: \"preferFlatMapOverMapFlat\",\n fix(fixer) {\n // Get the .map() call\n const mapCall = (node.callee as ASTNode).object\n const mapCallText = sourceCode.getText(mapCall)\n\n // Replace .map() with .flatMap() and remove .flat()\n const flatMapText = mapCallText.replace(/\\.map\\s*\\(/, \".flatMap(\")\n\n return fixer.replaceText(node, flatMapText)\n },\n })\n return // Don't check other patterns if we found .map().flat()\n }\n\n // Check for chained maps where intermediate results are arrays (highest priority after map().flat())\n if (node.callee.type === \"MemberExpression\" && node.callee.property.name === \"map\") {\n const object = node.callee.object\n if (\n object.type === \"CallExpression\" &&\n object.callee.type === \"MemberExpression\" &&\n object.callee.property.name === \"map\"\n ) {\n // Check if the first map returns arrays\n const firstMapCallback = object.arguments[0]\n if (firstMapCallback && returnsArray(firstMapCallback)) {\n context.report({\n node: object, // Report on the first map call\n messageId: \"preferFlatMapChain\",\n })\n return // Don't check other patterns for this chain\n }\n }\n }\n\n // Check for nested maps that return arrays (but not if they're part of map().flat() or chains)\n if (checkNestedMaps && isNestedMapReturningArrays(node)) {\n // Don't flag if this map is immediately followed by flat()\n if (\n node.parent &&\n node.parent.type === \"MemberExpression\" &&\n node.parent.parent &&\n node.parent.parent.type === \"CallExpression\" &&\n node.parent.property.name === \"flat\"\n ) {\n return // Skip - this will be handled by the map().flat() rule\n }\n\n // Don't flag if this map is part of a chain (either as first or second map)\n const object = node.callee?.object\n if (\n object?.type === \"CallExpression\" &&\n object.callee?.type === \"MemberExpression\" &&\n object.callee?.property?.name === \"map\"\n ) {\n return // Skip - this is part of a chain\n }\n\n // Check if this map feeds into another map\n if (\n node.parent?.type === \"MemberExpression\" &&\n node.parent.parent?.type === \"CallExpression\" &&\n node.parent.parent.callee?.property?.name === \"map\"\n ) {\n return // Skip - this feeds into a chain\n }\n\n context.report({\n node,\n messageId: \"preferFlatMapNested\",\n })\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"AAIA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,KAAM,CACJ,YAAa,kEACb,YAAa,GACd,CACD,QAAS,OACT,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,gBAAiB,CACf,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,yBAA0B,0CAC1B,oBAAqB,oEACrB,mBAAoB,2EACrB,CACF,CAED,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,EAAE,EACR,kBAAoB,GAEpD,SAAS,EAAoB,EAAwB,CACnD,GAAI,EAAK,OAAS,iBAAkB,MAAO,GAE3C,IAAM,EAAS,EAAK,OACpB,GAAI,EAAO,OAAS,mBAAoB,MAAO,GAG/C,GAAI,EAAO,SAAS,OAAS,OAAQ,CACnC,IAAM,EAAS,EAAO,OAGtB,GACE,EAAO,OAAS,kBAChB,EAAO,OAAO,OAAS,oBACvB,EAAO,OAAO,SAAS,OAAS,MAEhC,MAAO,GAIX,MAAO,GAGT,SAAS,EAAa,EAAgC,CACpD,GAAI,CAAC,GAAgB,CAAC,EAAa,KAAM,MAAO,GAGhD,GAAI,EAAa,KAAK,OAAS,kBAC7B,MAAO,GAIT,GAAI,EAAa,KAAK,OAAS,iBAAkB,CAC/C,IAAM,EAAO,EAAa,KAC1B,GAAI,EAAK,OAAO,OAAS,mBAAoB,CAC3C,IAAM,EAAa,EAAK,OAAO,SAAS,KAExC,GAAI,CAAC,MAAO,SAAU,QAAS,SAAU,QAAQ,CAAC,SAAS,EAAW,CACpE,MAAO,IAMb,GAAI,EAAa,KAAK,OAAS,iBAAkB,CAC/C,IAAM,EAAa,EAAa,KAAK,KAGrC,IAAK,IAAM,KAAQ,EACjB,GAAI,EAAK,OAAS,mBAAqB,EAAK,SAAU,CACpD,GAAI,EAAK,SAAS,OAAS,kBACzB,MAAO,GAIT,GAAI,EAAK,SAAS,OAAS,iBAAkB,CAC3C,IAAM,EAAO,EAAK,SAClB,GAAI,EAAK,OAAO,OAAS,mBAAoB,CAC3C,IAAM,EAAa,EAAK,OAAO,SAAS,KAExC,GAAI,CAAC,MAAO,SAAU,QAAS,SAAU,QAAQ,CAAC,SAAS,EAAW,CACpE,MAAO,MAQnB,MAAO,GAGT,SAAS,EAA2B,EAAwB,CAC1D,GAAI,EAAK,OAAS,iBAAkB,MAAO,GAE3C,IAAM,EAAS,EAAK,OACpB,GAAI,EAAO,OAAS,mBAAoB,MAAO,GAG/C,GAAI,EAAO,SAAS,OAAS,MAAO,CAClC,IAAM,EAAW,EAAK,UAAU,GAChC,GAAI,IAAa,EAAS,OAAS,2BAA6B,EAAS,OAAS,sBAChF,OAAO,EAAa,EAAS,CAIjC,MAAO,GAGT,MAAO,CACL,eAAe,EAAe,CAE5B,GAAI,EAAoB,EAAK,CAAE,CAC7B,IAAM,EAAa,EAAQ,WAE3B,EAAQ,OAAO,CACb,OACA,UAAW,2BACX,IAAI,EAAO,CAET,IAAM,EAAW,EAAK,OAAmB,OAInC,EAHc,EAAW,QAAQ,
|
|
1
|
+
{"version":3,"file":"prefer-flatmap.js","names":[],"sources":["../../src/rules/prefer-flatmap.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 .flatMap() over .map().flat() and nested transformations\",\n recommended: true,\n },\n fixable: \"code\",\n schema: [\n {\n type: \"object\",\n properties: {\n checkNestedMaps: {\n type: \"boolean\",\n default: true,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferFlatMapOverMapFlat: \"Use .flatMap() instead of .map().flat()\",\n preferFlatMapNested: \"Consider .flatMap() for nested transformations that return arrays\",\n preferFlatMapChain: \"Use .flatMap() instead of chained .map() operations that flatten results\",\n },\n },\n\n create(context) {\n const options = context.options[0] || {}\n const checkNestedMaps = options.checkNestedMaps !== false\n\n function isMapFollowedByFlat(node: ASTNode): boolean {\n if (node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n if (callee.type !== \"MemberExpression\") return false\n\n // Check if this is .flat()\n if (callee.property.name === \"flat\") {\n const object = callee.object\n\n // Check if the object is a .map() call\n if (\n object.type === \"CallExpression\" &&\n object.callee.type === \"MemberExpression\" &&\n object.callee.property.name === \"map\"\n ) {\n return true\n }\n }\n\n return false\n }\n\n function returnsArray(functionNode: ASTNode): boolean {\n if (!functionNode || !functionNode.body) return false\n\n // Arrow function with expression body\n if (functionNode.body.type === \"ArrayExpression\") {\n return true\n }\n\n // Arrow function with call expression body\n if (functionNode.body.type === \"CallExpression\") {\n const call = functionNode.body\n if (call.callee.type === \"MemberExpression\") {\n const methodName = call.callee.property.name\n // Common methods that return arrays\n if ([\"map\", \"filter\", \"slice\", \"concat\", \"split\"].includes(methodName)) {\n return true\n }\n }\n }\n\n // Function with block body\n if (functionNode.body.type === \"BlockStatement\") {\n const statements = functionNode.body.body\n\n // Look for return statements that return arrays\n for (const stmt of statements) {\n if (stmt.type === \"ReturnStatement\" && stmt.argument) {\n if (stmt.argument.type === \"ArrayExpression\") {\n return true\n }\n\n // Check for method calls that return arrays\n if (stmt.argument.type === \"CallExpression\") {\n const call = stmt.argument\n if (call.callee.type === \"MemberExpression\") {\n const methodName = call.callee.property.name\n // Common methods that return arrays\n if ([\"map\", \"filter\", \"slice\", \"concat\", \"split\"].includes(methodName)) {\n return true\n }\n }\n }\n }\n }\n }\n\n return false\n }\n\n function isNestedMapReturningArrays(node: ASTNode): boolean {\n if (node.type !== \"CallExpression\") return false\n\n const callee = node.callee\n if (callee.type !== \"MemberExpression\") return false\n\n // Check if this is .map()\n if (callee.property.name === \"map\") {\n const callback = node.arguments[0]\n if (callback && (callback.type === \"ArrowFunctionExpression\" || callback.type === \"FunctionExpression\")) {\n return returnsArray(callback)\n }\n }\n\n return false\n }\n\n return {\n CallExpression(node: ASTNode) {\n // Check for .map().flat() pattern first (highest priority)\n if (isMapFollowedByFlat(node)) {\n const sourceCode = context.sourceCode\n\n context.report({\n node,\n messageId: \"preferFlatMapOverMapFlat\",\n fix(fixer) {\n // Get the .map() call\n const mapCall = (node.callee as ASTNode).object\n const mapCallText = sourceCode.getText(mapCall)\n\n // Replace .map() with .flatMap() and remove .flat()\n const flatMapText = mapCallText.replace(/\\.map\\s*\\(/, \".flatMap(\")\n\n return fixer.replaceText(node, flatMapText)\n },\n })\n return // Don't check other patterns if we found .map().flat()\n }\n\n // Check for chained maps where intermediate results are arrays (highest priority after map().flat())\n if (node.callee.type === \"MemberExpression\" && node.callee.property.name === \"map\") {\n const object = node.callee.object\n if (\n object.type === \"CallExpression\" &&\n object.callee.type === \"MemberExpression\" &&\n object.callee.property.name === \"map\"\n ) {\n // Check if the first map returns arrays\n const firstMapCallback = object.arguments[0]\n if (firstMapCallback && returnsArray(firstMapCallback)) {\n context.report({\n node: object, // Report on the first map call\n messageId: \"preferFlatMapChain\",\n })\n return // Don't check other patterns for this chain\n }\n }\n }\n\n // Check for nested maps that return arrays (but not if they're part of map().flat() or chains)\n if (checkNestedMaps && isNestedMapReturningArrays(node)) {\n // Don't flag if this map is immediately followed by flat()\n if (\n node.parent &&\n node.parent.type === \"MemberExpression\" &&\n node.parent.parent &&\n node.parent.parent.type === \"CallExpression\" &&\n node.parent.property.name === \"flat\"\n ) {\n return // Skip - this will be handled by the map().flat() rule\n }\n\n // Don't flag if this map is part of a chain (either as first or second map)\n const object = node.callee?.object\n if (\n object?.type === \"CallExpression\" &&\n object.callee?.type === \"MemberExpression\" &&\n object.callee?.property?.name === \"map\"\n ) {\n return // Skip - this is part of a chain\n }\n\n // Check if this map feeds into another map\n if (\n node.parent?.type === \"MemberExpression\" &&\n node.parent.parent?.type === \"CallExpression\" &&\n node.parent.parent.callee?.property?.name === \"map\"\n ) {\n return // Skip - this feeds into a chain\n }\n\n context.report({\n node,\n messageId: \"preferFlatMapNested\",\n })\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"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"}
|
|
@@ -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":"0LAMA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,gFACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,aAAc,CACZ,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,kBAAmB,kEACnB,yBAA0B,mDAC1B,gBAAiB,2BACjB,aAAc,2BACd,eAAgB,wBAChB,iBAAkB,sCACnB,CACF,CAED,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,EAAE,EACX,eAAiB,GAE9C,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,EAAS,EAChD,EAAS,SAAS,YAAY,EAC9B,EAAS,SAAS,SAAS,EAC3B,EAAS,SAAS,UAAU,CAIhC,IAAM,EAAkB,EAAyB,EAAQ,CAEzD,SAAS,GAAqC,CAC5C,OAAO,EAAkB,EAAQ,WAAY,MAAM,CAGrD,MAAO,CACL,cAAc,EAAe,CAU3B,GATI,GAAgB,GAAc,EAG9B,CAAC,EAAK,QAAU,EAAK,OAAO,OAAS,cAAgB,EAAK,OAAO,OAAS,OAG1E,GAA2B,EAG3B,EAAuB,EAAM,EAAgB,CAAE,OAEnD,IAAM,EAAa,EAAQ,WACrB,EAAO,EAAK,UAEZ,EAAiD,EAAE,CAEzD,GAAI,EAAK,SAAW,EAElB,EAAY,KAAK,CACf,UAAW,kBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,cAAc,EAEhD,CAAC,SACO,EAAK,SAAW,GAAK,EAAK,GAAG,OAAS,kBAAmB,CAKlE,IAAM,EAHW,EAAK,GACI,SACE,IAAK,GAAgB,EAAW,QAAQ,EAAG,CAAC,
|
|
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":"0LAMA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,gFACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,aAAc,CACZ,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,kBAAmB,kEACnB,yBAA0B,mDAC1B,gBAAiB,2BACjB,aAAc,2BACd,eAAgB,wBAChB,iBAAkB,sCACnB,CACF,CAED,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,EAAE,EACX,eAAiB,GAE9C,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,EAAS,EAChD,EAAS,SAAS,YAAY,EAC9B,EAAS,SAAS,SAAS,EAC3B,EAAS,SAAS,UAAU,CAIhC,IAAM,EAAkB,EAAyB,EAAQ,CAEzD,SAAS,GAAqC,CAC5C,OAAO,EAAkB,EAAQ,WAAY,MAAM,CAGrD,MAAO,CACL,cAAc,EAAe,CAU3B,GATI,GAAgB,GAAc,EAG9B,CAAC,EAAK,QAAU,EAAK,OAAO,OAAS,cAAgB,EAAK,OAAO,OAAS,OAG1E,GAA2B,EAG3B,EAAuB,EAAM,EAAgB,CAAE,OAEnD,IAAM,EAAa,EAAQ,WACrB,EAAO,EAAK,UAEZ,EAAiD,EAAE,CAEzD,GAAI,EAAK,SAAW,EAElB,EAAY,KAAK,CACf,UAAW,kBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,cAAc,EAEhD,CAAC,SACO,EAAK,SAAW,GAAK,EAAK,GAAG,OAAS,kBAAmB,CAKlE,IAAM,EAHW,EAAK,GACI,SACE,IAAK,GAAgB,EAAW,QAAQ,EAAG,CAC3C,CAAC,KAAK,KAAK,CACvC,EAAY,KAAK,CACf,UAAW,eACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,UAAU,EAAU,GAAG,EAEzD,CAAC,SACO,EAAK,SAAW,EAAG,CAE5B,IAAM,EAAU,EAAW,QAAQ,EAAK,GAAG,CAC3C,EAAY,KAAK,CACf,UAAW,iBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,OAAO,EAAQ,GAAG,EAEpD,CAAC,MAGF,EAAY,KAAK,CACf,UAAW,kBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,cAAc,EAEhD,CAAC,CAGC,GAA2B,EAC9B,EAAY,KAAK,CACf,UAAW,mBACX,KAAM,CAAE,OAAQ,MAAO,CACvB,IAAK,EAAkB,EAAY,MAAM,CAC1C,CAAC,CAGJ,EAAQ,OAAO,CACb,OACA,UAAW,2BACX,QAAS,EACV,CAAC,EAGJ,gBAAgB,EAAe,CAG7B,GAFI,GAAgB,GAAc,EAE9B,CAAC,EAAK,SAAU,OAEpB,IAAM,EAAa,EAAQ,WAM3B,IALiB,EAAK,SAAS,OAAS,aAAe,EAAK,SAAS,KAAO,EAAW,QAAQ,EAAK,SAAS,IAE5F,OAGb,GAA2B,CAAE,OAGjC,IAAI,EAAU,IACV,EAAY,IACV,EAAa,EAAK,eAAe,QAAU,EAAK,gBAAgB,OAClE,GAAc,EAAW,QAAU,GACrC,EAAU,EAAW,QAAQ,EAAW,GAAG,CAC3C,EAAY,EAAW,QAAQ,EAAW,GAAG,EACpC,GAAc,EAAW,SAAW,IAC7C,EAAU,EAAW,QAAQ,EAAW,GAAG,EAG7C,IAAM,EAAiD,CACrD,CACE,UAAW,mBACX,KAAM,CAAE,OAAQ,MAAO,CACvB,IAAK,EAAkB,EAAY,MAAM,CAC1C,CACF,CAED,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,UAAS,YAAW,CAC5B,QAAS,EACV,CAAC,EAEL,EAEJ"}
|
|
@@ -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":"0LAMA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,mEACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,aAAc,CACZ,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,kBAAmB,gDACnB,yBAA0B,mDAC1B,gBAAiB,2BACjB,aAAc,2BACd,eAAgB,wBAChB,iBAAkB,sCACnB,CACF,CAED,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,EAAE,EACX,eAAiB,GAE9C,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,EAAS,EAChD,EAAS,SAAS,YAAY,EAC9B,EAAS,SAAS,SAAS,EAC3B,EAAS,SAAS,UAAU,CAIhC,IAAM,EAAkB,EAAyB,EAAQ,CAEzD,SAAS,GAA4B,CACnC,OAAO,EAAkB,EAAQ,WAAY,MAAM,CAGrD,MAAO,CACL,cAAc,EAAe,CAU3B,GATI,GAAgB,GAAc,EAG9B,CAAC,EAAK,QAAU,EAAK,OAAO,OAAS,cAAgB,EAAK,OAAO,OAAS,OAG1E,GAA2B,EAG3B,EAAuB,EAAM,EAAgB,CAAE,OAEnD,IAAM,EAAa,EAAQ,WACrB,EAAO,EAAK,WAAa,EAAE,CAC3B,EAAiD,EAAE,CAEzD,GAAI,EAAK,SAAW,EAElB,EAAY,KAAK,CACf,UAAW,kBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,cAAc,EAEhD,CAAC,SACO,EAAK,SAAW,GAAK,EAAK,GAAG,OAAS,kBAAmB,CAKlE,IAAM,GAHY,EAAK,GACI,UAAY,EAAE,EACX,IAAK,GAAgB,EAAW,QAAQ,EAAG,CAAC,
|
|
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":"0LAMA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,mEACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,aAAc,CACZ,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,kBAAmB,gDACnB,yBAA0B,mDAC1B,gBAAiB,2BACjB,aAAc,2BACd,eAAgB,wBAChB,iBAAkB,sCACnB,CACF,CAED,OAAO,EAAS,CAEd,IAAM,GADU,EAAQ,QAAQ,IAAM,EAAE,EACX,eAAiB,GAE9C,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,EAAS,EAChD,EAAS,SAAS,YAAY,EAC9B,EAAS,SAAS,SAAS,EAC3B,EAAS,SAAS,UAAU,CAIhC,IAAM,EAAkB,EAAyB,EAAQ,CAEzD,SAAS,GAA4B,CACnC,OAAO,EAAkB,EAAQ,WAAY,MAAM,CAGrD,MAAO,CACL,cAAc,EAAe,CAU3B,GATI,GAAgB,GAAc,EAG9B,CAAC,EAAK,QAAU,EAAK,OAAO,OAAS,cAAgB,EAAK,OAAO,OAAS,OAG1E,GAA2B,EAG3B,EAAuB,EAAM,EAAgB,CAAE,OAEnD,IAAM,EAAa,EAAQ,WACrB,EAAO,EAAK,WAAa,EAAE,CAC3B,EAAiD,EAAE,CAEzD,GAAI,EAAK,SAAW,EAElB,EAAY,KAAK,CACf,UAAW,kBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,cAAc,EAEhD,CAAC,SACO,EAAK,SAAW,GAAK,EAAK,GAAG,OAAS,kBAAmB,CAKlE,IAAM,GAHY,EAAK,GACI,UAAY,EAAE,EACX,IAAK,GAAgB,EAAW,QAAQ,EAAG,CAC5C,CAAC,KAAK,KAAK,CACxC,EAAY,KAAK,CACf,UAAW,eACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,UAAU,EAAS,GAAG,EAExD,CAAC,SACO,EAAK,SAAW,EAAG,CAE5B,IAAM,EAAU,EAAW,QAAQ,EAAK,GAAG,CAC3C,EAAY,KAAK,CACf,UAAW,iBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,OAAO,EAAQ,GAAG,EAEpD,CAAC,MAGF,EAAY,KAAK,CACf,UAAW,kBACX,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,cAAc,EAEhD,CAAC,CAGC,GAA2B,EAC9B,EAAY,KAAK,CACf,UAAW,mBACX,KAAM,CAAE,OAAQ,MAAO,CACvB,IAAK,EAAkB,EAAY,MAAM,CAC1C,CAAC,CAGJ,EAAQ,OAAO,CACb,OACA,UAAW,2BACX,QAAS,EACV,CAAC,EAGJ,gBAAgB,EAAe,CAG7B,GAFI,GAAgB,GAAc,EAE9B,CAAC,EAAK,SAAU,OAEpB,IAAM,EAAa,EAAQ,WAM3B,IALiB,EAAK,SAAS,OAAS,aAAe,EAAK,SAAS,KAAO,EAAW,QAAQ,EAAK,SAAS,IAE5F,OAGb,GAA2B,CAAE,OAGjC,IAAI,EAAY,IACZ,EAAK,gBAAgB,SAAS,GAChC,EAAY,EAAW,QAAQ,EAAK,eAAe,OAAO,GAAG,CACpD,EAAK,eAAe,SAAS,KACtC,EAAY,EAAW,QAAQ,EAAK,cAAc,OAAO,GAAG,EAG9D,IAAM,EAAiD,CACrD,CACE,UAAW,mBACX,KAAM,CAAE,OAAQ,MAAO,CACvB,IAAK,EAAkB,EAAY,MAAM,CAC1C,CACF,CAED,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,KAAM,EAAW,CACzB,QAAS,EACV,CAAC,EAEL,EAEJ"}
|
|
@@ -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\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":"kLAMA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,8DACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,mBAAoB,CAClB,KAAM,UACN,QAAS,GACV,CACD,oBAAqB,CACnB,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,WAAY,sDACZ,kBAAmB,6DACnB,gBAAiB,8BACjB,cAAe,4BACf,iBAAkB,sCACnB,CACF,CAED,OAAO,EAAS,CACd,IAAM,EAAU,EAAQ,QAAQ,IAAM,EAAE,CAClC,EAAqB,EAAQ,qBAAuB,GACpD,EAAsB,EAAQ,sBAAwB,GAGtD,EAAkB,EAAyB,EAAQ,CAEzD,SAAS,GAAe,CACtB,IAAM,EAAW,EAAQ,SACzB,MACE,kCAAkC,KAAK,EAAS,EAChD,EAAS,SAAS,YAAY,EAC9B,EAAS,SAAS,SAAS,EAC3B,EAAS,SAAS,UAAU,CAIhC,SAAS,EAAkB,EAAe,EAAsD,CAE9F,SAAS,EAAW,EAA2B,CAC7C,GAAI,EAAE,OAAS,gCAAkC,EAAE,QAAU,EAAE,OAAO,GACpE,OAAO,EAAW,QAAQ,EAAE,OAAO,GAAG,CAIxC,IAAK,IAAM,KAAO,EAAG,CACnB,GAAI,IAAQ,SAAU,SACtB,IAAM,EAAQ,EAAE,GAChB,GAAI,MAAM,QAAQ,EAAM,MACjB,IAAM,KAAQ,EACjB,GAAI,GAAQ,OAAO,GAAS,UAAY,EAAK,KAAM,CACjD,IAAM,EAAS,EAAW,EAAK,CAC/B,GAAI,EAAQ,OAAO,WAGd,GAAS,OAAO,GAAU,UAAY,EAAM,KAAM,CAC3D,IAAM,EAAS,EAAW,EAAM,CAChC,GAAI,EAAQ,OAAO,GAIvB,OAAO,KAGT,OAAO,EAAW,EAAK,CAGzB,MAAO,CACL,YAAY,EAAe,CACzB,GAAI,GAAsB,GAAc,CAAE,OAE1C,IAAM,EAAa,EAAQ,WACrB,EAAc,EAAW,QAAQ,EAAK,YAAY,CAClD,EAAW,EAAW,QAAQ,EAAK,CAEnC,EAA6C,CACjD,CACE,UAAW,kBACX,KAAM,CAAE,KAAM,EAAa,CAC3B,IAAI,EAAuB,CACzB,OAAO,EAAM,YAAY,EAAM,QAAQ,EAAY,GAAG,EAEzD,CACF,CAEI,EAAkB,EAAY,OAAO,EACxC,EAAQ,KAAK,CACX,UAAW,mBACX,KAAM,CAAE,OAAQ,OAAQ,CACxB,IAAK,EAAkB,EAAY,OAAO,CAC3C,CAAC,CAGJ,EAAQ,OAAO,CACb,OACA,UAAW,aACX,KAAM,CACJ,KAAM,EACN,UAAW,EACZ,CACD,UACD,CAAC,EAGJ,gBAAgB,EAAe,CAC7B,GAAI,GAAsB,GAAc,CAAE,OAE1C,IAAM,EAAa,EAAQ,WAG3B,GAAI,CAAC,EAAK,SAAU,OAEpB,IAAM,EAAW,EAAK,SAAS,OAAS,aAAe,EAAK,SAAS,KAAO,EAAW,QAAQ,EAAK,SAAS,CAG7G,GAAI,IAAa,QAAS,CAExB,IAAM,EAAY,EAAkB,EAAM,EAAW,CAC/C,EAAW,EAAW,QAAQ,EAAK,CAGzC,GAAI,GAAuB,EAAS,WAAW,WAAW,CAAE,OAE5D,IAAM,EAAe,GAAa,IAE5B,EAA6C,CACjD,CACE,UAAW,kBACX,KAAM,CAAE,KAAM,EAAc,CAC5B,IAAI,EAAuB,CACzB,OAAO,EAAM,YAAY,EAAM,QAAQ,EAAa,GAAG,EAE1D,CACF,CAEI,EAAkB,EAAY,OAAO,EACxC,EAAQ,KAAK,CACX,UAAW,mBACX,KAAM,CAAE,OAAQ,OAAQ,CACxB,IAAK,EAAkB,EAAY,OAAO,CAC3C,CAAC,CAGJ,EAAQ,OAAO,CACb,OACA,UAAW,aACX,KAAM,CACJ,KAAM,EACN,UAAW,EACZ,CACD,UACD,CAAC,CAIJ,GAAI,IAAa,gBAAiB,CAChC,IAAM,EAAY,EAAkB,EAAM,EAAW,CAC/C,EAAW,EAAW,QAAQ,EAAK,CACnC,EAAe,GAAa,IAE5B,EAA6C,CACjD,CACE,UAAW,kBACX,KAAM,CAAE,KAAM,EAAc,CAC5B,IAAI,EAAuB,CACzB,OAAO,EAAM,YAAY,EAAM,QAAQ,EAAa,GAAG,EAE1D,CACF,CAEI,EAAkB,EAAY,OAAO,EACxC,EAAQ,KAAK,CACX,UAAW,mBACX,KAAM,CAAE,OAAQ,OAAQ,CACxB,IAAK,EAAkB,EAAY,OAAO,CAC3C,CAAC,CAGJ,EAAQ,OAAO,CACb,OACA,UAAW,aACX,KAAM,CACJ,KAAM,EACN,UAAW,EACZ,CACD,UACD,CAAC,GAIN,gBAAgB,EAAe,CAI7B,GAHI,GAAsB,GAAc,EAGpC,EAAK,SAAS,SAAW,EAAG,OAGhC,IAAI,EAAS,EAAK,OAMlB,GALI,GAAU,EAAe,EAAQ,EAAgB,EAMnD,GACA,EAAO,OAAS,kBAChB,EAAO,OAAO,OAAS,oBACvB,EAAO,OAAO,OAAO,OAAS,cAC9B,EAAO,OAAO,OAAO,OAAS,QAC9B,CAAC,OAAQ,KAAK,CAAC,SAAS,EAAO,OAAO,SAAS,KAAK,CAEpD,OAMF,IADA,EAAS,EAAK,OACP,GAAQ,CACb,GAAI,EAAO,OAAS,kBAClB,OAEF,EAAS,EAAO,OAKlB,IAAI,EAAoB,GAExB,IADA,EAAS,EAAK,OACP,GAAQ,CACb,GAAI,EAAO,OAAS,sBAAwB,EAAO,IAAI,eAAgB,CAErE,EAAoB,GACpB,MAEF,GAAI,EAAO,OAAS,mBAAoB,CACtC,EAAoB,GACpB,MAEF,EAAS,EAAO,OAGlB,GAAI,EAAmB,OAKvB,GAFkB,EAAK,SAAS,KAAM,GAAO,IAAO,MAAQ,EAAG,OAAS,gBAE3D,CAAE,CACb,EAAQ,OAAO,CACb,OACA,UAAW,oBACZ,CAAC,CACF,OAGF,IAAM,EAAa,EAAQ,WACrB,EAAe,EAAK,SAAS,OAAQ,GAAO,IAAO,KAAK,CAAC,IAAK,GAAO,EAAW,QAAQ,EAAG,CAAC,CAE5F,EAA6C,CACjD,CACE,UAAW,gBACX,IAAI,EAAuB,CACzB,OAAO,EAAM,YAAY,EAAM,WAAW,EAAa,KAAK,KAAK,CAAC,GAAG,EAExE,CACF,CAEI,EAAkB,EAAY,OAAO,EACxC,EAAQ,KAAK,CACX,UAAW,mBACX,KAAM,CAAE,OAAQ,OAAQ,CACxB,IAAK,EAAkB,EAAY,OAAO,CAC3C,CAAC,CAGJ,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,UACD,CAAC,EAEL,EAEJ"}
|
|
@@ -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":"AAIA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,KAAM,CACJ,YAAa,oEACb,YAAa,GACd,CACD,QAAS,OACT,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,kBAAmB,CACjB,KAAM,UACN,QAAS,GACV,CACD,cAAe,CACb,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,kBAAmB,8DACnB,kBAAmB,4CACnB,eAAgB,6EACjB,CACF,CAED,OAAO,EAAS,CACd,IAAM,EAAU,EAAQ,QAAQ,IAAM,EAAE,CAClC,EAAoB,EAAQ,oBAAsB,GAClD,EAAgB,EAAQ,gBAAkB,GAEhD,SAAS,EAAmB,EAAwB,CAGlD,GACE,EAAK,OAAS,kBACd,EAAK,OAAO,OAAS,oBACrB,EAAK,OAAO,SAAS,OAAS,UAE9B,MAAO,GAIT,IAAM,EAAW,EAAK,UAAU,GAMhC,OALI,IAAa,EAAS,OAAS,2BAA6B,EAAS,OAAS,sBACnE,EAAS,KAEV,OAAS,mBAEhB,GAGT,SAAS,EAAqB,EAAwB,CACpD,GAAI,CAAC,EAAK,MAAQ,EAAK,KAAK,OAAS,iBAAkB,MAAO,GAE9D,IAAM,EAAa,EAAK,KAAK,KAI7B,OAHI,EAAW,SAAW,EAAU,GAG7B,EAAW,KAAM,GAAkB,CACxC,GAAI,EAAK,OAAS,uBAAyB,EAAK,WAAW,OAAS,iBAAkB,CACpF,IAAM,EAAO,EAAK,WAClB,OAAO,EAAK,OAAO,OAAS,oBAAsB,EAAK,OAAO,SAAS,OAAS,OAElF,MAAO,IACP,CAGJ,SAAS,EAAuB,EAAwB,CAGtD,GACE,EAAK,OAAS,kBACd,EAAK,OAAO,OAAS,oBACrB,EAAK,OAAO,SAAS,OAAS,UAC9B,CACA,IAAM,EAAW,EAAK,UAAU,GAChC,GAAI,IAAa,EAAS,OAAS,2BAA6B,EAAS,OAAS,uBACnE,EAAS,KAEb,OAAS,mBAChB,MAAO,GAIb,MAAO,GAGT,MAAO,CACL,aAAa,EAAe,CACrB,GAED,EAAqB,EAAK,EAC5B,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,WAAY,QAAS,CAC9B,CAAC,EAIN,eAAe,EAAe,CACvB,GAED,EAAqB,EAAK,EAC5B,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,WAAY,SAAU,CAC/B,CAAC,EAIN,eAAe,EAAe,CACvB,GAED,EAAqB,EAAK,EAC5B,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,WAAY,WAAY,CACjC,CAAC,EAIN,eAAe,EAAe,CACvB,OAGD,EAAK,OAAO,OAAS,oBAAsB,EAAK,OAAO,SAAS,OAAS,WACvE,EAAuB,EAAK,EAC9B,EAAQ,OAAO,CACb,OACA,UAAW,iBACX,IAAI,EAAO,CAET,GAAI,CAAC,EAAmB,EAAK,CAC3B,OAAO,KAOT,IAAM,EAJa,EAAQ,WACH,QAAQ,
|
|
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":"AAIA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,KAAM,CACJ,YAAa,oEACb,YAAa,GACd,CACD,QAAS,OACT,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,kBAAmB,CACjB,KAAM,UACN,QAAS,GACV,CACD,cAAe,CACb,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,kBAAmB,8DACnB,kBAAmB,4CACnB,eAAgB,6EACjB,CACF,CAED,OAAO,EAAS,CACd,IAAM,EAAU,EAAQ,QAAQ,IAAM,EAAE,CAClC,EAAoB,EAAQ,oBAAsB,GAClD,EAAgB,EAAQ,gBAAkB,GAEhD,SAAS,EAAmB,EAAwB,CAGlD,GACE,EAAK,OAAS,kBACd,EAAK,OAAO,OAAS,oBACrB,EAAK,OAAO,SAAS,OAAS,UAE9B,MAAO,GAIT,IAAM,EAAW,EAAK,UAAU,GAMhC,OALI,IAAa,EAAS,OAAS,2BAA6B,EAAS,OAAS,sBACnE,EAAS,KAEV,OAAS,mBAEhB,GAGT,SAAS,EAAqB,EAAwB,CACpD,GAAI,CAAC,EAAK,MAAQ,EAAK,KAAK,OAAS,iBAAkB,MAAO,GAE9D,IAAM,EAAa,EAAK,KAAK,KAI7B,OAHI,EAAW,SAAW,EAAU,GAG7B,EAAW,KAAM,GAAkB,CACxC,GAAI,EAAK,OAAS,uBAAyB,EAAK,WAAW,OAAS,iBAAkB,CACpF,IAAM,EAAO,EAAK,WAClB,OAAO,EAAK,OAAO,OAAS,oBAAsB,EAAK,OAAO,SAAS,OAAS,OAElF,MAAO,IACP,CAGJ,SAAS,EAAuB,EAAwB,CAGtD,GACE,EAAK,OAAS,kBACd,EAAK,OAAO,OAAS,oBACrB,EAAK,OAAO,SAAS,OAAS,UAC9B,CACA,IAAM,EAAW,EAAK,UAAU,GAChC,GAAI,IAAa,EAAS,OAAS,2BAA6B,EAAS,OAAS,uBACnE,EAAS,KAEb,OAAS,mBAChB,MAAO,GAIb,MAAO,GAGT,MAAO,CACL,aAAa,EAAe,CACrB,GAED,EAAqB,EAAK,EAC5B,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,WAAY,QAAS,CAC9B,CAAC,EAIN,eAAe,EAAe,CACvB,GAED,EAAqB,EAAK,EAC5B,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,WAAY,SAAU,CAC/B,CAAC,EAIN,eAAe,EAAe,CACvB,GAED,EAAqB,EAAK,EAC5B,EAAQ,OAAO,CACb,OACA,UAAW,oBACX,KAAM,CAAE,WAAY,WAAY,CACjC,CAAC,EAIN,eAAe,EAAe,CACvB,OAGD,EAAK,OAAO,OAAS,oBAAsB,EAAK,OAAO,SAAS,OAAS,WACvE,EAAuB,EAAK,EAC9B,EAAQ,OAAO,CACb,OACA,UAAW,iBACX,IAAI,EAAO,CAET,GAAI,CAAC,EAAmB,EAAK,CAC3B,OAAO,KAOT,IAAM,EAJa,EAAQ,WACH,QAAQ,EAGV,CAAC,QAAQ,kBAAmB,QAAQ,CAC1D,OAAO,EAAM,YAAY,EAAM,EAAU,EAE5C,CAAC,CAKF,EAAK,OAAO,OAAS,oBAAsB,EAAK,OAAO,SAAS,OAAS,QAAQ,CAEnF,IAAI,EAAS,EAAK,OAClB,KAAO,GAAQ,CACb,GACE,EAAO,OAAS,kBAChB,EAAO,OAAO,OAAS,qBACtB,EAAO,OAAO,SAAS,OAAS,WAAa,EAAO,OAAO,SAAS,OAAS,OAC9E,CACA,EAAQ,OAAO,CACb,OACA,UAAW,oBACZ,CAAC,CACF,MAEF,EAAS,EAAO,UAIvB,EAEJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-option.js","names":[],"sources":["../../src/rules/prefer-option.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImportsLegacy, isAlreadyUsingFunctype, isFunctypeType } 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 Option<T> over nullable types (T | null | undefined)\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowNullableIntersections: {\n type: \"boolean\",\n default: false,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferOption: \"Prefer Option<{{type}}> over nullable type '{{nullable}}'\",\n preferOptionReturn: \"Prefer Option<{{type}}> as return type over nullable '{{nullable}}'\",\n suggestOptionType: \"Replace with Option<{{type}}>\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n // const options = context.options[0] || {}\n // Remove unused variable\n // const _allowNullableIntersections = options.allowNullableIntersections || false\n\n // Get functype imports if available (but still apply rule even without explicit import)\n const functypeImports = getFunctypeImportsLegacy(context)\n\n return {\n TSUnionType(node: ASTNode) {\n if (!node.types || node.types.length < 2) return\n\n const hasNull = node.types.some(\n (type: ASTNode) => type.type === \"TSNullKeyword\" || type.type === \"TSUndefinedKeyword\",\n )\n\n if (!hasNull) return\n\n const nonNullTypes = node.types.filter(\n (type: ASTNode) => type.type !== \"TSNullKeyword\" && type.type !== \"TSUndefinedKeyword\",\n )\n\n if (nonNullTypes.length === 1) {\n const nonNullType = nonNullTypes[0]\n\n // Skip if it's already an Option type or other functype type\n if (isFunctypeType(nonNullType, functypeImports)) return\n\n // Skip if we're already in a functype context\n if (isAlreadyUsingFunctype(node, functypeImports)) return\n\n const sourceCode = context.sourceCode\n const nonNullTypeText = sourceCode.getText(nonNullType)\n const fullType = sourceCode.getText(node)\n\n // Skip if it's already an Option type (fallback check)\n if (nonNullTypeText.startsWith(\"Option<\")) return\n\n const suggestions: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestOptionType\",\n data: { type: nonNullTypeText },\n fix(fixer) {\n return fixer.replaceText(node, `Option<${nonNullTypeText}>`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"Option\")) {\n suggestions.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Option\" },\n fix: createImportFixer(sourceCode, \"Option\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferOption\",\n data: {\n type: nonNullTypeText,\n nullable: fullType,\n },\n suggest: suggestions,\n })\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"8MAMA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,8DACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,2BAA4B,CAC1B,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,aAAc,4DACd,mBAAoB,sEACpB,kBAAmB,gCACnB,iBAAkB,sCACnB,CACF,CAED,OAAO,EAAS,CAMd,IAAM,EAAkB,EAAyB,EAAQ,CAEzD,MAAO,CACL,YAAY,EAAe,CAOzB,GANI,CAAC,EAAK,OAAS,EAAK,MAAM,OAAS,GAMnC,CAJY,EAAK,MAAM,KACxB,GAAkB,EAAK,OAAS,iBAAmB,EAAK,OAAS,
|
|
1
|
+
{"version":3,"file":"prefer-option.js","names":[],"sources":["../../src/rules/prefer-option.ts"],"sourcesContent":["import type { Rule } from \"eslint\"\n\nimport type { ASTNode } from \"../types/ast\"\nimport { getFunctypeImportsLegacy, isAlreadyUsingFunctype, isFunctypeType } 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 Option<T> over nullable types (T | null | undefined)\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowNullableIntersections: {\n type: \"boolean\",\n default: false,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferOption: \"Prefer Option<{{type}}> over nullable type '{{nullable}}'\",\n preferOptionReturn: \"Prefer Option<{{type}}> as return type over nullable '{{nullable}}'\",\n suggestOptionType: \"Replace with Option<{{type}}>\",\n suggestAddImport: \"Add {{symbol}} import from functype\",\n },\n },\n\n create(context) {\n // const options = context.options[0] || {}\n // Remove unused variable\n // const _allowNullableIntersections = options.allowNullableIntersections || false\n\n // Get functype imports if available (but still apply rule even without explicit import)\n const functypeImports = getFunctypeImportsLegacy(context)\n\n return {\n TSUnionType(node: ASTNode) {\n if (!node.types || node.types.length < 2) return\n\n const hasNull = node.types.some(\n (type: ASTNode) => type.type === \"TSNullKeyword\" || type.type === \"TSUndefinedKeyword\",\n )\n\n if (!hasNull) return\n\n const nonNullTypes = node.types.filter(\n (type: ASTNode) => type.type !== \"TSNullKeyword\" && type.type !== \"TSUndefinedKeyword\",\n )\n\n if (nonNullTypes.length === 1) {\n const nonNullType = nonNullTypes[0]\n\n // Skip if it's already an Option type or other functype type\n if (isFunctypeType(nonNullType, functypeImports)) return\n\n // Skip if we're already in a functype context\n if (isAlreadyUsingFunctype(node, functypeImports)) return\n\n const sourceCode = context.sourceCode\n const nonNullTypeText = sourceCode.getText(nonNullType)\n const fullType = sourceCode.getText(node)\n\n // Skip if it's already an Option type (fallback check)\n if (nonNullTypeText.startsWith(\"Option<\")) return\n\n const suggestions: Rule.SuggestionReportDescriptor[] = [\n {\n messageId: \"suggestOptionType\",\n data: { type: nonNullTypeText },\n fix(fixer) {\n return fixer.replaceText(node, `Option<${nonNullTypeText}>`)\n },\n },\n ]\n\n if (!hasFunctypeSymbol(sourceCode, \"Option\")) {\n suggestions.push({\n messageId: \"suggestAddImport\",\n data: { symbol: \"Option\" },\n fix: createImportFixer(sourceCode, \"Option\"),\n })\n }\n\n context.report({\n node,\n messageId: \"preferOption\",\n data: {\n type: nonNullTypeText,\n nullable: fullType,\n },\n suggest: suggestions,\n })\n }\n },\n }\n },\n}\n\nexport default rule\n"],"mappings":"8MAMA,MAAM,EAAwB,CAC5B,KAAM,CACJ,KAAM,aACN,eAAgB,GAChB,KAAM,CACJ,YAAa,8DACb,YAAa,GACd,CACD,OAAQ,CACN,CACE,KAAM,SACN,WAAY,CACV,2BAA4B,CAC1B,KAAM,UACN,QAAS,GACV,CACF,CACD,qBAAsB,GACvB,CACF,CACD,SAAU,CACR,aAAc,4DACd,mBAAoB,sEACpB,kBAAmB,gCACnB,iBAAkB,sCACnB,CACF,CAED,OAAO,EAAS,CAMd,IAAM,EAAkB,EAAyB,EAAQ,CAEzD,MAAO,CACL,YAAY,EAAe,CAOzB,GANI,CAAC,EAAK,OAAS,EAAK,MAAM,OAAS,GAMnC,CAJY,EAAK,MAAM,KACxB,GAAkB,EAAK,OAAS,iBAAmB,EAAK,OAAS,qBAGxD,CAAE,OAEd,IAAM,EAAe,EAAK,MAAM,OAC7B,GAAkB,EAAK,OAAS,iBAAmB,EAAK,OAAS,qBACnE,CAED,GAAI,EAAa,SAAW,EAAG,CAC7B,IAAM,EAAc,EAAa,GAMjC,GAHI,EAAe,EAAa,EAAgB,EAG5C,EAAuB,EAAM,EAAgB,CAAE,OAEnD,IAAM,EAAa,EAAQ,WACrB,EAAkB,EAAW,QAAQ,EAAY,CACjD,EAAW,EAAW,QAAQ,EAAK,CAGzC,GAAI,EAAgB,WAAW,UAAU,CAAE,OAE3C,IAAM,EAAiD,CACrD,CACE,UAAW,oBACX,KAAM,CAAE,KAAM,EAAiB,CAC/B,IAAI,EAAO,CACT,OAAO,EAAM,YAAY,EAAM,UAAU,EAAgB,GAAG,EAE/D,CACF,CAEI,EAAkB,EAAY,SAAS,EAC1C,EAAY,KAAK,CACf,UAAW,mBACX,KAAM,CAAE,OAAQ,SAAU,CAC1B,IAAK,EAAkB,EAAY,SAAS,CAC7C,CAAC,CAGJ,EAAQ,OAAO,CACb,OACA,UAAW,eACX,KAAM,CACJ,KAAM,EACN,SAAU,EACX,CACD,QAAS,EACV,CAAC,GAGP,EAEJ"}
|
|
@@ -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,GAOjB,MAAO,GAOT,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,EAAW,CAC3C,OAAO,KAGT,GAAI,GAA0B,EAAuB,OAAS,oBAAqB,CACjF,IAAM,EAAa,EAInB,GADqB,EAAW,YAAY,KAAM,GAAM,EAAE,OAAS,2BAA2B,EAAI,GAEhG,OAAO,KAKT,IAAM,GADa,EAAW,YAAc,EAAE,EACX,OAAQ,GAAM,EAAE,OAAS,kBAAkB,CAE9E,GAAI,EAAgB,OAAS,EAAG,CAC9B,IAAM,EAAgB,EAAgB,EAAgB,OAAS,GAC/D,GAAI,EACF,OAAO,EAAM,gBAAgB,EAAe,KAAK,IAAa,CAMlE,IAAM,EADa,EAAW,QAAQ,
|
|
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,GAOjB,MAAO,GAOT,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,EAAW,CAC3C,OAAO,KAGT,GAAI,GAA0B,EAAuB,OAAS,oBAAqB,CACjF,IAAM,EAAa,EAInB,GADqB,EAAW,YAAY,KAAM,GAAM,EAAE,OAAS,2BAA2B,EAAI,GAEhG,OAAO,KAKT,IAAM,GADa,EAAW,YAAc,EAAE,EACX,OAAQ,GAAM,EAAE,OAAS,kBAAkB,CAE9E,GAAI,EAAgB,OAAS,EAAG,CAC9B,IAAM,EAAgB,EAAgB,EAAgB,OAAS,GAC/D,GAAI,EACF,OAAO,EAAM,gBAAgB,EAAe,KAAK,IAAa,CAMlE,IAAM,EADa,EAAW,QAAQ,EACZ,CAAC,QAAQ,YAAa,KAAK,EAAW,IAAI,CACpE,OAAO,EAAM,YAAY,EAAY,EAAQ,CAI/C,IAAM,EAAY,YAAY,EAAW,oBAEzC,GAAI,EACF,OAAO,EAAM,gBAAgB,EAAgB,KAAK,IAAY,CAIhE,IAAM,EAAY,EAAQ,KAAK,GAK/B,OAJI,EACK,EAAM,iBAAiB,EAAW,GAAG,EAAU,IAAI,CAGrD,EAAM,sBAAsB,CAAC,EAAG,EAAE,CAAE,GAAG,EAAU,IAAI"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-functype",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Custom ESLint rules for functional TypeScript programming with functype library patterns including Do notation (ESLint 10+)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,17 +34,37 @@
|
|
|
34
34
|
"either",
|
|
35
35
|
"list"
|
|
36
36
|
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"validate": "ts-builds validate",
|
|
39
|
+
"format": "ts-builds format",
|
|
40
|
+
"format:check": "ts-builds format:check",
|
|
41
|
+
"lint": "ts-builds lint",
|
|
42
|
+
"lint:check": "ts-builds lint:check",
|
|
43
|
+
"typecheck": "ts-builds typecheck",
|
|
44
|
+
"test": "ts-builds test",
|
|
45
|
+
"test:watch": "ts-builds test:watch",
|
|
46
|
+
"test:ui": "ts-builds test:ui",
|
|
47
|
+
"test:coverage": "ts-builds test:coverage",
|
|
48
|
+
"build": "ts-builds build",
|
|
49
|
+
"dev": "ts-builds dev",
|
|
50
|
+
"prepublishOnly": "pnpm validate",
|
|
51
|
+
"list-rules": "node dist/cli/list-rules.js",
|
|
52
|
+
"list-rules:verbose": "node dist/cli/list-rules.js --verbose",
|
|
53
|
+
"list-rules:usage": "node dist/cli/list-rules.js --usage",
|
|
54
|
+
"check-deps": "node dist/cli/list-rules.js --check-deps",
|
|
55
|
+
"cli:help": "node dist/cli/list-rules.js --help"
|
|
56
|
+
},
|
|
37
57
|
"prettier": "ts-builds/prettier",
|
|
38
58
|
"peerDependencies": {
|
|
39
|
-
"eslint": "^10.
|
|
59
|
+
"eslint": "^10.2.1"
|
|
40
60
|
},
|
|
41
61
|
"devDependencies": {
|
|
42
62
|
"@types/node": "^24.12.2",
|
|
43
|
-
"@typescript-eslint/rule-tester": "^8.
|
|
63
|
+
"@typescript-eslint/rule-tester": "^8.59.1",
|
|
44
64
|
"eslint-config-prettier": "^10.1.8",
|
|
45
|
-
"functype": "^0.
|
|
46
|
-
"ts-builds": "^2.
|
|
47
|
-
"tsdown": "^0.21.
|
|
65
|
+
"functype": "^0.60.2",
|
|
66
|
+
"ts-builds": "^2.7.1",
|
|
67
|
+
"tsdown": "^0.21.10"
|
|
48
68
|
},
|
|
49
69
|
"author": {
|
|
50
70
|
"name": "Jordan Burke",
|
|
@@ -61,25 +81,7 @@
|
|
|
61
81
|
},
|
|
62
82
|
"homepage": "https://github.com/jordanburke/eslint-functype#readme",
|
|
63
83
|
"engines": {
|
|
64
|
-
"node": ">=
|
|
84
|
+
"node": ">=24.0.0"
|
|
65
85
|
},
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
"format": "ts-builds format",
|
|
69
|
-
"format:check": "ts-builds format:check",
|
|
70
|
-
"lint": "ts-builds lint",
|
|
71
|
-
"lint:check": "ts-builds lint:check",
|
|
72
|
-
"typecheck": "ts-builds typecheck",
|
|
73
|
-
"test": "ts-builds test",
|
|
74
|
-
"test:watch": "ts-builds test:watch",
|
|
75
|
-
"test:ui": "ts-builds test:ui",
|
|
76
|
-
"test:coverage": "ts-builds test:coverage",
|
|
77
|
-
"build": "ts-builds build",
|
|
78
|
-
"dev": "ts-builds dev",
|
|
79
|
-
"list-rules": "node dist/cli/list-rules.js",
|
|
80
|
-
"list-rules:verbose": "node dist/cli/list-rules.js --verbose",
|
|
81
|
-
"list-rules:usage": "node dist/cli/list-rules.js --usage",
|
|
82
|
-
"check-deps": "node dist/cli/list-rules.js --check-deps",
|
|
83
|
-
"cli:help": "node dist/cli/list-rules.js --help"
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
+
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
|
87
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023 Jordan
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|