eslint-plugin-ts-data-forge 0.2.0 → 0.4.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.
Files changed (63) hide show
  1. package/README.md +16 -14
  2. package/dist/plugin.d.mts +3 -1
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +1 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +16 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rules/import-utils.mjs.map +1 -1
  9. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  10. package/dist/rules/no-unnecessary-type-guard.mjs +13 -1
  11. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  12. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +1 -1
  13. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  14. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +1 -1
  15. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  16. package/dist/rules/prefer-arr-is-max-length-array.mjs +1 -1
  17. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  18. package/dist/rules/prefer-arr-is-min-length-array.mjs +1 -1
  19. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-sum.mjs +2 -2
  21. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  22. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  23. package/dist/rules/prefer-canonical-length-cast.d.mts +6 -0
  24. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -0
  25. package/dist/rules/prefer-canonical-length-cast.mjs +155 -0
  26. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -0
  27. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  28. package/dist/rules/prefer-canonical-length-guard.mjs +12 -8
  29. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  30. package/dist/rules/prefer-is-record-and-has-key.d.mts +1 -1
  31. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  32. package/dist/rules/prefer-is-record-and-has-key.mjs +30 -16
  33. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  34. package/dist/rules/prefer-range-for-loop.mjs +1 -1
  35. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  36. package/dist/rules/rules.d.mts +2 -1
  37. package/dist/rules/rules.d.mts.map +1 -1
  38. package/dist/rules/rules.mjs +2 -0
  39. package/dist/rules/rules.mjs.map +1 -1
  40. package/package.json +6 -6
  41. package/src/plugin.mts +1 -0
  42. package/src/rule-types.mts +17 -1
  43. package/src/rules/import-utils.mts +0 -1
  44. package/src/rules/no-unnecessary-type-guard.mts +65 -0
  45. package/src/rules/no-unnecessary-type-guard.test.mts +94 -0
  46. package/src/rules/prefer-arr-is-bounded-length-array.mts +1 -1
  47. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +5 -5
  48. package/src/rules/prefer-arr-is-fixed-length-array.mts +1 -1
  49. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +11 -11
  50. package/src/rules/prefer-arr-is-max-length-array.mts +1 -1
  51. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -8
  52. package/src/rules/prefer-arr-is-min-length-array.mts +1 -1
  53. package/src/rules/prefer-arr-is-min-length-array.test.mts +9 -9
  54. package/src/rules/prefer-arr-sum.mts +2 -2
  55. package/src/rules/prefer-canonical-array-slicing.mts +0 -2
  56. package/src/rules/prefer-canonical-length-cast.mts +290 -0
  57. package/src/rules/prefer-canonical-length-cast.test.mts +305 -0
  58. package/src/rules/prefer-canonical-length-guard.mts +40 -21
  59. package/src/rules/prefer-canonical-length-guard.test.mts +32 -32
  60. package/src/rules/prefer-is-record-and-has-key.mts +84 -20
  61. package/src/rules/prefer-is-record-and-has-key.test.mts +74 -0
  62. package/src/rules/prefer-range-for-loop.mts +1 -1
  63. package/src/rules/rules.mts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-canonical-length-guard.mjs","sources":["../../src/rules/prefer-canonical-length-guard.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { getImportedLocalName, getTsDataForgeImport } from './import-utils.mjs';\nimport { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.mjs';\nimport { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.mjs';\nimport { preferArrIsMaxLengthArray } from './prefer-arr-is-max-length-array.mjs';\nimport { preferArrIsMinLengthArray } from './prefer-arr-is-min-length-array.mjs';\nimport { preferArrIsNonEmpty } from './prefer-arr-is-non-empty.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds =\n | 'useCanonicalGuard'\n | 'useIsBoundedLengthArray'\n | 'useIsFixedLengthArray'\n | 'useIsMaxLengthArray'\n | 'useIsMinLengthArray'\n | 'useIsNonEmpty';\n\n/**\n * The `xs.length <op> n` → `Arr.is*` rules folded into this one.\n *\n * Their implementations are reused as-is rather than rewritten: this rule\n * merges their visitors, so every case they already cover (type-aware array\n * checks, `Arr` import insertion, the `&&`-bounded-range special case) keeps\n * behaving exactly as before, and their co-located tests keep exercising them\n * directly.\n */\nconst COMPARISON_RULES = [\n preferArrIsNonEmpty,\n preferArrIsMinLengthArray,\n preferArrIsMaxLengthArray,\n preferArrIsBoundedLengthArray,\n preferArrIsFixedLengthArray,\n] as const;\n\nconst MESSAGES = {\n useCanonicalGuard:\n 'Replace `{{arrName}}.{{guard}}({{boundsText}})` with `{{arrName}}.{{replacement}}(...)`: the bound makes the two guards equivalent.',\n useIsNonEmpty: preferArrIsNonEmpty.meta.messages.useIsNonEmpty,\n useIsMinLengthArray:\n preferArrIsMinLengthArray.meta.messages.useIsMinLengthArray,\n useIsMaxLengthArray:\n preferArrIsMaxLengthArray.meta.messages.useIsMaxLengthArray,\n useIsBoundedLengthArray:\n preferArrIsBoundedLengthArray.meta.messages.useIsBoundedLengthArray,\n useIsFixedLengthArray:\n preferArrIsFixedLengthArray.meta.messages.useIsFixedLengthArray,\n} as const satisfies Record<MessageIds, string>;\n\n/**\n * Length guards whose bound makes them redundant with `Arr.isEmpty` /\n * `Arr.isNonEmpty`, keyed by guard name.\n *\n * `isEmpty` narrows to `FixedLengthArray<0, E> & Xs` and `isNonEmpty` to\n * `MinLengthArray<1, E> & Xs`, so:\n *\n * - The `*Array` entries are **type-identical** — `isFixedLengthArray(xs, 0)`\n * and `isMinLengthArray(xs, 1)` produce exactly those types.\n * - The `*Tuple` entries **narrow**: they resolve to the structural\n * `readonly []` / `readonly [E, ...E[]]`, and the canonical guards add the\n * brand on top. The result stays assignable everywhere the old type was, so\n * the rewrite is safe, but it is a strengthening rather than a pure rename.\n */\nconst GUARD_REWRITES = [\n { guard: 'isFixedLengthArray', bounds: [0], replacement: 'isEmpty' },\n { guard: 'isFixedLengthTuple', bounds: [0], replacement: 'isEmpty' },\n { guard: 'isMaxLengthTuple', bounds: [0], replacement: 'isEmpty' },\n { guard: 'isMaxLengthArray', bounds: [0], replacement: 'isEmpty' },\n { guard: 'isBoundedLengthTuple', bounds: [0, 0], replacement: 'isEmpty' },\n { guard: 'isBoundedLengthArray', bounds: [0, 0], replacement: 'isEmpty' },\n { guard: 'isMinLengthArray', bounds: [1], replacement: 'isNonEmpty' },\n { guard: 'isMinLengthTuple', bounds: [1], replacement: 'isNonEmpty' },\n] as const satisfies readonly Readonly<{\n guard: string;\n bounds: readonly number[];\n replacement: string;\n}>[];\n\nexport const preferCanonicalLengthGuard: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Normalize array-length checks to their canonical `Arr` guard: `xs.length <op> n` becomes the matching `Arr.is*` guard, and degenerate guards (e.g. `Arr.isFixedLengthTuple(xs, 0)`) become `Arr.isEmpty` / `Arr.isNonEmpty`.',\n },\n fixable: 'code',\n schema: [],\n messages: MESSAGES,\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const arrLocalName = getImportedLocalName(\n getTsDataForgeImport(sourceCode.ast),\n 'Arr',\n );\n\n const comparisonVisitors = COMPARISON_RULES.map((rule) =>\n rule.create(context),\n );\n\n if (arrLocalName === undefined) return mergeVisitors(comparisonVisitors);\n\n return mergeVisitors([\n ...comparisonVisitors,\n {\n CallExpression: (node) => {\n const guardName = getGuardName(node, arrLocalName);\n\n if (guardName === undefined) return;\n\n const rewrite = GUARD_REWRITES.find(\n (entry) => entry.guard === guardName,\n );\n\n if (rewrite === undefined || !matchesBounds(node, rewrite.bounds)) {\n return;\n }\n\n const [array] = node.arguments;\n\n if (array === undefined) return;\n\n const arrayText = sourceCode.getText(array);\n\n context.report({\n node,\n messageId: 'useCanonicalGuard',\n data: {\n arrName: arrLocalName,\n guard: guardName,\n boundsText: ['…', ...rewrite.bounds.map(String)].join(', '),\n replacement: rewrite.replacement,\n },\n fix: (fixer) =>\n fixer.replaceText(\n node,\n `${arrLocalName}.${rewrite.replacement}(${arrayText})`,\n ),\n });\n },\n },\n ]);\n },\n defaultOptions: [],\n} as const;\n\n/**\n * Combines several rule listeners into one, calling every handler registered\n * for a given selector in order.\n */\nconst mergeVisitors = (\n // `RuleListener` holds mutable AST handler signatures, so it is not deeply\n // readonly.\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n visitors: readonly TSESLint.RuleListener[],\n): TSESLint.RuleListener => {\n const mut_merged: Record<string, ((node: never) => void)[]> = {};\n\n for (const visitor of visitors) {\n for (const [selector, handler] of Object.entries(visitor)) {\n if (typeof handler !== 'function') continue;\n\n const mut_handlers = mut_merged[selector] ?? [];\n\n mut_handlers.push(handler);\n\n mut_merged[selector] = mut_handlers;\n }\n }\n\n return Object.fromEntries(\n Object.entries(mut_merged).map(([selector, handlers]) => [\n selector,\n (node: never) => {\n for (const handler of handlers) {\n handler(node);\n }\n },\n ]),\n );\n};\n\n/** The `Arr.<guard>` method name of `node`, or `undefined` for other calls. */\nconst getGuardName = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: TSESTree.CallExpression,\n arrLocalName: string,\n): string | undefined => {\n const { callee } = node;\n\n return callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.object.type === AST_NODE_TYPES.Identifier &&\n callee.object.name === arrLocalName &&\n callee.property.type === AST_NODE_TYPES.Identifier\n ? callee.property.name\n : undefined;\n};\n\n/**\n * Whether the call is `guard(array, ...bounds)` with every bound written as the\n * exact numeric literal the rewrite requires.\n */\nconst matchesBounds = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: TSESTree.CallExpression,\n bounds: readonly number[],\n): boolean => {\n const args = node.arguments;\n\n if (args.length !== bounds.length + 1 || node.typeArguments !== undefined) {\n return false;\n }\n\n return args.every(\n (arg, index) =>\n arg.type !== AST_NODE_TYPES.SpreadElement &&\n (index === 0 ||\n (arg.type === AST_NODE_TYPES.Literal &&\n arg.value === bounds[index - 1])),\n );\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prefer-canonical-length-guard.mjs","sources":["../../src/rules/prefer-canonical-length-guard.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { getImportedLocalName, getTsDataForgeImport } from './import-utils.mjs';\nimport { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.mjs';\nimport { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.mjs';\nimport { preferArrIsMaxLengthArray } from './prefer-arr-is-max-length-array.mjs';\nimport { preferArrIsMinLengthArray } from './prefer-arr-is-min-length-array.mjs';\nimport { preferArrIsNonEmpty } from './prefer-arr-is-non-empty.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds =\n | 'useCanonicalGuard'\n | 'useIsBoundedLengthArray'\n | 'useIsFixedLengthArray'\n | 'useIsMaxLengthArray'\n | 'useIsMinLengthArray'\n | 'useIsNonEmpty';\n\n/**\n * The `xs.length <op> n` → `Arr.is*` rules folded into this one.\n *\n * Their implementations are reused as-is rather than rewritten: this rule\n * merges their visitors, so every case they already cover (type-aware array\n * checks, `Arr` import insertion, the `&&`-bounded-range special case) keeps\n * behaving exactly as before, and their co-located tests keep exercising them\n * directly.\n */\nconst COMPARISON_RULES = [\n preferArrIsNonEmpty,\n preferArrIsMinLengthArray,\n preferArrIsMaxLengthArray,\n preferArrIsBoundedLengthArray,\n preferArrIsFixedLengthArray,\n] as const;\n\nconst MESSAGES = {\n useCanonicalGuard:\n 'Replace `{{arrName}}.{{guard}}({{boundsText}})` with `{{arrName}}.{{replacement}}(...)`: the bound makes the two guards equivalent.',\n useIsNonEmpty: preferArrIsNonEmpty.meta.messages.useIsNonEmpty,\n useIsMinLengthArray:\n preferArrIsMinLengthArray.meta.messages.useIsMinLengthArray,\n useIsMaxLengthArray:\n preferArrIsMaxLengthArray.meta.messages.useIsMaxLengthArray,\n useIsBoundedLengthArray:\n preferArrIsBoundedLengthArray.meta.messages.useIsBoundedLengthArray,\n useIsFixedLengthArray:\n preferArrIsFixedLengthArray.meta.messages.useIsFixedLengthArray,\n} as const satisfies Record<MessageIds, string>;\n\n/**\n * Length guards whose bound makes them redundant with a named degenerate\n * guard, keyed by guard name.\n *\n * Every entry stays inside its own family rather than crossing the\n * branded/structural divide:\n *\n * - the branded `*Array` guards go to `isEmpty` / `isNonEmpty`, which narrow to\n * `FixedLengthArray<0, E> & Xs` / `MinLengthArray<1, E> & Xs`;\n * - the structural `*Tuple` guards go to `isEmptyTuple` / `isNonEmptyTuple`,\n * which narrow to `readonly []` / `MinLengthTuple<1, E>`.\n *\n * Most entries are then **type-identical** — `isFixedLengthArray(0, xs)` and\n * `isMinLengthArray(1, xs)` produce exactly what `isEmpty` / `isNonEmpty`\n * narrow to, and `FixedLengthTuple<0, E>`, `MaxLengthTuple<0, E>` and\n * `BoundedLengthTuple<0, 0, E>` are all `readonly []`. Two *strengthen*:\n * `MaxLengthArray<0, E>` and `BoundedLengthArray<0, 0, E>` do not include the\n * `readonly []` structural part that `FixedLengthArray<0, E>` intersects on, so\n * rewriting them to `isEmpty` adds it. That is sound for a guard — the narrowed\n * type only flows outwards, and the stronger type is assignable everywhere the\n * old one was — and it stays within the branded family.\n *\n * Sending a `*Tuple` guard to the branded `isEmpty` would also be sound, but it\n * would silently change which family the value belongs to, which is why each\n * family keeps its own lane.\n */\nconst GUARD_REWRITES = [\n { guard: 'isFixedLengthArray', bounds: [0], replacement: 'isEmpty' },\n { guard: 'isMaxLengthArray', bounds: [0], replacement: 'isEmpty' },\n { guard: 'isBoundedLengthArray', bounds: [0, 0], replacement: 'isEmpty' },\n { guard: 'isMinLengthArray', bounds: [1], replacement: 'isNonEmpty' },\n { guard: 'isFixedLengthTuple', bounds: [0], replacement: 'isEmptyTuple' },\n { guard: 'isMaxLengthTuple', bounds: [0], replacement: 'isEmptyTuple' },\n {\n guard: 'isBoundedLengthTuple',\n bounds: [0, 0],\n replacement: 'isEmptyTuple',\n },\n { guard: 'isMinLengthTuple', bounds: [1], replacement: 'isNonEmptyTuple' },\n] as const satisfies readonly Readonly<{\n guard: string;\n bounds: readonly number[];\n replacement: string;\n}>[];\n\nexport const preferCanonicalLengthGuard: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Normalize array-length checks to their canonical `Arr` guard: `xs.length <op> n` becomes the matching `Arr.is*` guard, and a degenerate guard becomes the named one for its family (`Arr.isFixedLengthArray(0, xs)` → `Arr.isEmpty`, `Arr.isFixedLengthTuple(0, xs)` → `Arr.isEmptyTuple`).',\n },\n fixable: 'code',\n schema: [],\n messages: MESSAGES,\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const arrLocalName = getImportedLocalName(\n getTsDataForgeImport(sourceCode.ast),\n 'Arr',\n );\n\n const comparisonVisitors = COMPARISON_RULES.map((rule) =>\n rule.create(context),\n );\n\n if (arrLocalName === undefined) return mergeVisitors(comparisonVisitors);\n\n return mergeVisitors([\n ...comparisonVisitors,\n {\n CallExpression: (node) => {\n const guardName = getGuardName(node, arrLocalName);\n\n if (guardName === undefined) return;\n\n const rewrite = GUARD_REWRITES.find(\n (entry) => entry.guard === guardName,\n );\n\n if (rewrite === undefined || !matchesBounds(node, rewrite.bounds)) {\n return;\n }\n\n // The array is the last argument; the bounds precede it.\n const array = node.arguments[rewrite.bounds.length];\n\n if (array === undefined) return;\n\n const arrayText = sourceCode.getText(array);\n\n context.report({\n node,\n messageId: 'useCanonicalGuard',\n data: {\n arrName: arrLocalName,\n guard: guardName,\n boundsText: [...rewrite.bounds.map(String), '…'].join(', '),\n replacement: rewrite.replacement,\n },\n fix: (fixer) =>\n fixer.replaceText(\n node,\n `${arrLocalName}.${rewrite.replacement}(${arrayText})`,\n ),\n });\n },\n },\n ]);\n },\n defaultOptions: [],\n} as const;\n\n/**\n * Combines several rule listeners into one, calling every handler registered\n * for a given selector in order.\n */\nconst mergeVisitors = (\n // `RuleListener` holds mutable AST handler signatures, so it is not deeply\n // readonly.\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n visitors: readonly TSESLint.RuleListener[],\n): TSESLint.RuleListener => {\n const mut_merged: Record<string, ((node: never) => void)[]> = {};\n\n for (const visitor of visitors) {\n for (const [selector, handler] of Object.entries(visitor)) {\n if (typeof handler !== 'function') continue;\n\n const mut_handlers = mut_merged[selector] ?? [];\n\n mut_handlers.push(handler);\n\n mut_merged[selector] = mut_handlers;\n }\n }\n\n return Object.fromEntries(\n Object.entries(mut_merged).map(([selector, handlers]) => [\n selector,\n (node: never) => {\n for (const handler of handlers) {\n handler(node);\n }\n },\n ]),\n );\n};\n\n/** The `Arr.<guard>` method name of `node`, or `undefined` for other calls. */\nconst getGuardName = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: TSESTree.CallExpression,\n arrLocalName: string,\n): string | undefined => {\n const { callee } = node;\n\n return callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.object.type === AST_NODE_TYPES.Identifier &&\n callee.object.name === arrLocalName &&\n callee.property.type === AST_NODE_TYPES.Identifier\n ? callee.property.name\n : undefined;\n};\n\n/**\n * Whether the call is `guard(...bounds, array)` with every bound written as the\n * exact numeric literal the rewrite requires.\n *\n * The arity check also excludes the curried form (`guard(...bounds)`): the\n * degenerate guards take only the array, so there is nothing to rewrite it to.\n */\nconst matchesBounds = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: TSESTree.CallExpression,\n bounds: readonly number[],\n): boolean => {\n const args = node.arguments;\n\n if (args.length !== bounds.length + 1 || node.typeArguments !== undefined) {\n return false;\n }\n\n return args.every(\n (arg, index) =>\n arg.type !== AST_NODE_TYPES.SpreadElement &&\n (index === bounds.length ||\n (arg.type === AST_NODE_TYPES.Literal && arg.value === bounds[index])),\n );\n};\n"],"names":[],"mappings":";;;;;;;;AA+BA,MAAM,gBAAA,GAAmB;AAAA,EACvB,mBAAA;AAAA,EACA,yBAAA;AAAA,EACA,yBAAA;AAAA,EACA,6BAAA;AAAA,EACA;AACF,CAAA;AAEA,MAAM,QAAA,GAAW;AAAA,EACf,iBAAA,EACE,qIAAA;AAAA,EACF,aAAA,EAAe,mBAAA,CAAoB,IAAA,CAAK,QAAA,CAAS,aAAA;AAAA,EACjD,mBAAA,EACE,yBAAA,CAA0B,IAAA,CAAK,QAAA,CAAS,mBAAA;AAAA,EAC1C,mBAAA,EACE,yBAAA,CAA0B,IAAA,CAAK,QAAA,CAAS,mBAAA;AAAA,EAC1C,uBAAA,EACE,6BAAA,CAA8B,IAAA,CAAK,QAAA,CAAS,uBAAA;AAAA,EAC9C,qBAAA,EACE,2BAAA,CAA4B,IAAA,CAAK,QAAA,CAAS;AAC9C,CAAA;AA4BA,MAAM,cAAA,GAAiB;AAAA,EACrB,EAAE,OAAO,oBAAA,EAAsB,MAAA,EAAQ,CAAC,CAAC,CAAA,EAAG,aAAa,SAAA,EAAU;AAAA,EACnE,EAAE,OAAO,kBAAA,EAAoB,MAAA,EAAQ,CAAC,CAAC,CAAA,EAAG,aAAa,SAAA,EAAU;AAAA,EACjE,EAAE,OAAO,sBAAA,EAAwB,MAAA,EAAQ,CAAC,CAAA,EAAG,CAAC,CAAA,EAAG,WAAA,EAAa,SAAA,EAAU;AAAA,EACxE,EAAE,OAAO,kBAAA,EAAoB,MAAA,EAAQ,CAAC,CAAC,CAAA,EAAG,aAAa,YAAA,EAAa;AAAA,EACpE,EAAE,OAAO,oBAAA,EAAsB,MAAA,EAAQ,CAAC,CAAC,CAAA,EAAG,aAAa,cAAA,EAAe;AAAA,EACxE,EAAE,OAAO,kBAAA,EAAoB,MAAA,EAAQ,CAAC,CAAC,CAAA,EAAG,aAAa,cAAA,EAAe;AAAA,EACtE;AAAA,IACE,KAAA,EAAO,sBAAA;AAAA,IACP,MAAA,EAAQ,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,IACb,WAAA,EAAa;AAAA,GACf;AAAA,EACA,EAAE,OAAO,kBAAA,EAAoB,MAAA,EAAQ,CAAC,CAAC,CAAA,EAAG,aAAa,iBAAA;AACzD,CAAA;AAMO,MAAM,0BAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,OAAA,EAAS,MAAA;AAAA,IACT,QAAQ,EAAC;AAAA,IACT,QAAA,EAAU;AAAA,GACZ;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,IAAA,MAAM,YAAA,GAAe,oBAAA;AAAA,MACnB,oBAAA,CAAqB,WAAW,GAAG,CAAA;AAAA,MACnC;AAAA,KACF;AAEA,IAAA,MAAM,qBAAqB,gBAAA,CAAiB,GAAA;AAAA,MAAI,CAAC,IAAA,KAC/C,IAAA,CAAK,MAAA,CAAO,OAAO;AAAA,KACrB;AAEA,IAAA,IAAI,YAAA,KAAiB,MAAA,EAAW,OAAO,aAAA,CAAc,kBAAkB,CAAA;AAEvE,IAAA,OAAO,aAAA,CAAc;AAAA,MACnB,GAAG,kBAAA;AAAA,MACH;AAAA,QACE,cAAA,EAAgB,CAAC,IAAA,KAAS;AACxB,UAAA,MAAM,SAAA,GAAY,YAAA,CAAa,IAAA,EAAM,YAAY,CAAA;AAEjD,UAAA,IAAI,cAAc,MAAA,EAAW;AAE7B,UAAA,MAAM,UAAU,cAAA,CAAe,IAAA;AAAA,YAC7B,CAAC,KAAA,KAAU,KAAA,CAAM,KAAA,KAAU;AAAA,WAC7B;AAEA,UAAA,IAAI,YAAY,MAAA,IAAa,CAAC,cAAc,IAAA,EAAM,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjE,YAAA;AAAA,UACF;AAGA,UAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,OAAO,MAAM,CAAA;AAElD,UAAA,IAAI,UAAU,MAAA,EAAW;AAEzB,UAAA,MAAM,SAAA,GAAY,UAAA,CAAW,OAAA,CAAQ,KAAK,CAAA;AAE1C,UAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,YACb,IAAA;AAAA,YACA,SAAA,EAAW,mBAAA;AAAA,YACX,IAAA,EAAM;AAAA,cACJ,OAAA,EAAS,YAAA;AAAA,cACT,KAAA,EAAO,SAAA;AAAA,cACP,UAAA,EAAY,CAAC,GAAG,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAM,CAAA,EAAG,QAAG,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AAAA,cAC1D,aAAa,OAAA,CAAQ;AAAA,aACvB;AAAA,YACA,GAAA,EAAK,CAAC,KAAA,KACJ,KAAA,CAAM,WAAA;AAAA,cACJ,IAAA;AAAA,cACA,GAAG,YAAY,CAAA,CAAA,EAAI,OAAA,CAAQ,WAAW,IAAI,SAAS,CAAA,CAAA;AAAA;AACrD,WACH,CAAA;AAAA,QACH;AAAA;AACF,KACD,CAAA;AAAA,EACH,CAAA;AAAA,EACA,gBAAgB;AAClB;AAMA,MAAM,aAAA,GAAgB,CAIpB,QAAA,KAC0B;AAC1B,EAAA,MAAM,aAAwD,EAAC;AAE/D,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,KAAA,MAAW,CAAC,QAAA,EAAU,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AACzD,MAAA,IAAI,OAAO,YAAY,UAAA,EAAY;AAEnC,MAAA,MAAM,YAAA,GAAe,UAAA,CAAW,QAAQ,CAAA,IAAK,EAAC;AAE9C,MAAA,YAAA,CAAa,KAAK,OAAO,CAAA;AAEzB,MAAA,UAAA,CAAW,QAAQ,CAAA,GAAI,YAAA;AAAA,IACzB;AAAA,EACF;AAEA,EAAA,OAAO,MAAA,CAAO,WAAA;AAAA,IACZ,MAAA,CAAO,QAAQ,UAAU,CAAA,CAAE,IAAI,CAAC,CAAC,QAAA,EAAU,QAAQ,CAAA,KAAM;AAAA,MACvD,QAAA;AAAA,MACA,CAAC,IAAA,KAAgB;AACf,QAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,UAAA,OAAA,CAAQ,IAAI,CAAA;AAAA,QACd;AAAA,MACF;AAAA,KACD;AAAA,GACH;AACF,CAAA;AAGA,MAAM,YAAA,GAAe,CAEnB,IAAA,EACA,YAAA,KACuB;AACvB,EAAA,MAAM,EAAE,QAAO,GAAI,IAAA;AAEnB,EAAA,OAAO,MAAA,CAAO,SAAS,cAAA,CAAe,gBAAA,IACpC,CAAC,MAAA,CAAO,QAAA,IACR,MAAA,CAAO,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,cACtC,MAAA,CAAO,MAAA,CAAO,IAAA,KAAS,YAAA,IACvB,MAAA,CAAO,QAAA,CAAS,SAAS,cAAA,CAAe,UAAA,GACtC,MAAA,CAAO,QAAA,CAAS,IAAA,GAChB,MAAA;AACN,CAAA;AASA,MAAM,aAAA,GAAgB,CAEpB,IAAA,EACA,MAAA,KACY;AACZ,EAAA,MAAM,OAAO,IAAA,CAAK,SAAA;AAElB,EAAA,IAAI,KAAK,MAAA,KAAW,MAAA,CAAO,SAAS,CAAA,IAAK,IAAA,CAAK,kBAAkB,MAAA,EAAW;AACzE,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,IAAA,CAAK,KAAA;AAAA,IACV,CAAC,GAAA,EAAK,KAAA,KACJ,GAAA,CAAI,IAAA,KAAS,eAAe,aAAA,KAC3B,KAAA,KAAU,MAAA,CAAO,MAAA,IACf,IAAI,IAAA,KAAS,cAAA,CAAe,WAAW,GAAA,CAAI,KAAA,KAAU,OAAO,KAAK,CAAA;AAAA,GACxE;AACF,CAAA;;;;"}
@@ -1,6 +1,6 @@
1
1
  import { type TSESLint } from '@typescript-eslint/utils';
2
2
  type Options = readonly [];
3
- type MessageIds = 'useIsRecordAndHasKey';
3
+ type MessageIds = 'useIsRecordAndHasKey' | 'useHasKey';
4
4
  export declare const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options>;
5
5
  export {};
6
6
  //# sourceMappingURL=prefer-is-record-and-has-key.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-is-record-and-has-key.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAOlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,sBAAsB,CAAC;AAEzC,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA4IjE,CAAC"}
1
+ {"version":3,"file":"prefer-is-record-and-has-key.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AASlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,sBAAsB,GAAG,WAAW,CAAC;AAEvD,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA4KjE,CAAC"}
@@ -1,4 +1,6 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { Arr } from 'ts-data-forge';
3
+ import * as ts from 'typescript';
2
4
  import { getTsDataForgeImport, getNamedImports, buildImportFixes } from './import-utils.mjs';
3
5
 
4
6
  const preferIsRecordAndHasKey = {
@@ -10,13 +12,21 @@ const preferIsRecordAndHasKey = {
10
12
  fixable: "code",
11
13
  schema: [],
12
14
  messages: {
13
- useIsRecordAndHasKey: "Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge."
15
+ useIsRecordAndHasKey: "Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.",
16
+ useHasKey: "Replace `{{original}}` with `hasKey({{objName}}, {{keyName}})` from ts-data-forge."
14
17
  }
15
18
  },
16
19
  create: (context) => {
17
20
  const sourceCode = context.sourceCode;
18
21
  const program = sourceCode.ast;
19
22
  const tsDataForgeImport = getTsDataForgeImport(program);
23
+ const services = sourceCode.parserServices;
24
+ const checker = services?.program?.getTypeChecker();
25
+ const isKnownRecord = (expression) => {
26
+ if (checker === void 0) return false;
27
+ const tsNode = services?.esTreeNodeToTSNodeMap?.get(expression);
28
+ return tsNode !== void 0 && isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode));
29
+ };
20
30
  const mut_nodesToFix = [];
21
31
  return {
22
32
  // Handle Object.hasOwn(obj, key)
@@ -51,37 +61,38 @@ const preferIsRecordAndHasKey = {
51
61
  },
52
62
  "Program:exit": () => {
53
63
  const namedImports = getNamedImports(tsDataForgeImport);
54
- const hasIsRecordImport = namedImports.includes("isRecord");
55
- const hasHasKeyImport = namedImports.includes("hasKey");
64
+ const rewrites = mut_nodesToFix.map((entry) => ({
65
+ ...entry,
66
+ // A record-typed object makes the `isRecord(...)` conjunct always
67
+ // true, so it is left out of the replacement entirely.
68
+ needsIsRecord: !isKnownRecord(entry.objExpression)
69
+ }));
70
+ const importsToAdd = [
71
+ ...rewrites.some(({ needsIsRecord }) => needsIsRecord) && !namedImports.includes("isRecord") ? ["isRecord"] : [],
72
+ ...namedImports.includes("hasKey") ? [] : ["hasKey"]
73
+ ];
56
74
  for (const [
57
75
  index,
58
- { node, objExpression, keyExpression }
59
- ] of mut_nodesToFix.entries()) {
76
+ { node, objExpression, keyExpression, needsIsRecord }
77
+ ] of rewrites.entries()) {
60
78
  const objText = sourceCode.getText(objExpression);
61
79
  const keyText = sourceCode.getText(keyExpression);
62
80
  const originalText = sourceCode.getText(node);
63
81
  context.report({
64
82
  node,
65
- messageId: "useIsRecordAndHasKey",
83
+ messageId: needsIsRecord ? "useIsRecordAndHasKey" : "useHasKey",
66
84
  data: {
67
85
  original: originalText,
68
86
  objName: objText,
69
87
  keyName: keyText
70
88
  },
71
89
  fix: (fixer) => {
72
- const replacement = `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`;
73
- const mut_importsToAdd = [];
74
- if (!hasIsRecordImport) {
75
- mut_importsToAdd.push("isRecord");
76
- }
77
- if (!hasHasKeyImport) {
78
- mut_importsToAdd.push("hasKey");
79
- }
80
- const importFixes = index === 0 && mut_importsToAdd.length > 0 ? buildImportFixes(
90
+ const replacement = needsIsRecord ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))` : `hasKey(${objText}, ${keyText})`;
91
+ const importFixes = index === 0 && importsToAdd.length > 0 ? buildImportFixes(
81
92
  fixer,
82
93
  program,
83
94
  tsDataForgeImport,
84
- mut_importsToAdd
95
+ importsToAdd
85
96
  ) : [];
86
97
  return [...importFixes, fixer.replaceText(node, replacement)];
87
98
  }
@@ -92,6 +103,9 @@ const preferIsRecordAndHasKey = {
92
103
  },
93
104
  defaultOptions: []
94
105
  };
106
+ const isAlreadyRecordType = (checker, type) => type.isUnion() ? type.types.every((member) => isAlreadyRecordType(checker, member)) : checker.getIndexInfoOfType(type, ts.IndexKind.String) !== void 0 && Arr.isEmpty(checker.getSignaturesOfType(type, ts.SignatureKind.Call)) && Arr.isEmpty(
107
+ checker.getSignaturesOfType(type, ts.SignatureKind.Construct)
108
+ ) && !checker.isArrayType(type) && !checker.isTupleType(type);
95
109
 
96
110
  export { preferIsRecordAndHasKey };
97
111
  //# sourceMappingURL=prefer-is-record-and-has-key.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-is-record-and-has-key.mjs","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useIsRecordAndHasKey';\n\nexport const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =\n {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `Object.hasOwn(obj, key)` or `key in obj` with `isRecord(obj) && hasKey(obj, key)` from ts-data-forge',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useIsRecordAndHasKey:\n 'Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsDataForgeImport = getTsDataForgeImport(program);\n\n const mut_nodesToFix: {\n node: TSESTree.CallExpression | TSESTree.BinaryExpression;\n objExpression: TSESTree.Expression;\n keyExpression: TSESTree.Expression;\n }[] = [];\n\n return {\n // Handle Object.hasOwn(obj, key)\n CallExpression: (node) => {\n if (\n node.callee.type !== AST_NODE_TYPES.MemberExpression ||\n node.callee.object.type !== AST_NODE_TYPES.Identifier ||\n node.callee.object.name !== 'Object' ||\n node.callee.property.type !== AST_NODE_TYPES.Identifier ||\n node.callee.property.name !== 'hasOwn'\n ) {\n return;\n }\n\n // Check arguments: Object.hasOwn(obj, key)\n if (node.arguments.length !== 2) return;\n\n const objArg = node.arguments[0];\n\n const keyArg = node.arguments[1];\n\n if (objArg === undefined || keyArg === undefined) return;\n\n // Type guard: ensure arguments are Expressions (not SpreadElement)\n if (\n objArg.type === AST_NODE_TYPES.SpreadElement ||\n keyArg.type === AST_NODE_TYPES.SpreadElement\n ) {\n return;\n }\n\n mut_nodesToFix.push({\n node,\n objExpression: objArg,\n keyExpression: keyArg,\n });\n },\n\n // Handle key in obj\n BinaryExpression: (node) => {\n if (node.operator !== 'in') return;\n\n const keyExpression = node.left;\n\n const objExpression = node.right;\n\n // Type guard: ensure keyExpression is Expression (not PrivateIdentifier)\n if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) return;\n\n mut_nodesToFix.push({\n node,\n objExpression,\n keyExpression,\n });\n },\n\n 'Program:exit': () => {\n const namedImports = getNamedImports(tsDataForgeImport);\n\n const hasIsRecordImport = namedImports.includes('isRecord');\n\n const hasHasKeyImport = namedImports.includes('hasKey');\n\n for (const [\n index,\n { node, objExpression, keyExpression },\n ] of mut_nodesToFix.entries()) {\n const objText = sourceCode.getText(objExpression);\n\n const keyText = sourceCode.getText(keyExpression);\n\n const originalText = sourceCode.getText(node);\n\n context.report({\n node,\n messageId: 'useIsRecordAndHasKey',\n data: {\n original: originalText,\n objName: objText,\n keyName: keyText,\n },\n fix: (fixer) => {\n const replacement = `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`;\n\n const mut_importsToAdd: string[] = [];\n\n if (!hasIsRecordImport) {\n mut_importsToAdd.push('isRecord');\n }\n\n if (!hasHasKeyImport) {\n mut_importsToAdd.push('hasKey');\n }\n\n const importFixes =\n index === 0 && mut_importsToAdd.length > 0\n ? buildImportFixes(\n fixer,\n program,\n tsDataForgeImport,\n mut_importsToAdd,\n )\n : [];\n\n return [...importFixes, fixer.replaceText(node, replacement)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n } as const;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prefer-is-record-and-has-key.mjs","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport * as ts from 'typescript';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useIsRecordAndHasKey' | 'useHasKey';\n\nexport const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =\n {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `Object.hasOwn(obj, key)` or `key in obj` with `isRecord(obj) && hasKey(obj, key)` from ts-data-forge',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useIsRecordAndHasKey:\n 'Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.',\n useHasKey:\n 'Replace `{{original}}` with `hasKey({{objName}}, {{keyName}})` from ts-data-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsDataForgeImport = getTsDataForgeImport(program);\n\n const services = sourceCode.parserServices;\n\n const checker = services?.program?.getTypeChecker();\n\n /**\n * Whether the `isRecord(...)` half of the rewrite would be dead code\n * because the object already satisfies `hasKey`'s\n * `R extends UnknownRecord` constraint. Without type information nothing\n * is known, so the guard is kept.\n */\n const isKnownRecord = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n expression: TSESTree.Expression,\n ): boolean => {\n if (checker === undefined) return false;\n\n const tsNode = services?.esTreeNodeToTSNodeMap?.get(expression);\n\n return (\n tsNode !== undefined &&\n isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))\n );\n };\n\n const mut_nodesToFix: {\n node: TSESTree.CallExpression | TSESTree.BinaryExpression;\n objExpression: TSESTree.Expression;\n keyExpression: TSESTree.Expression;\n }[] = [];\n\n return {\n // Handle Object.hasOwn(obj, key)\n CallExpression: (node) => {\n if (\n node.callee.type !== AST_NODE_TYPES.MemberExpression ||\n node.callee.object.type !== AST_NODE_TYPES.Identifier ||\n node.callee.object.name !== 'Object' ||\n node.callee.property.type !== AST_NODE_TYPES.Identifier ||\n node.callee.property.name !== 'hasOwn'\n ) {\n return;\n }\n\n // Check arguments: Object.hasOwn(obj, key)\n if (node.arguments.length !== 2) return;\n\n const objArg = node.arguments[0];\n\n const keyArg = node.arguments[1];\n\n if (objArg === undefined || keyArg === undefined) return;\n\n // Type guard: ensure arguments are Expressions (not SpreadElement)\n if (\n objArg.type === AST_NODE_TYPES.SpreadElement ||\n keyArg.type === AST_NODE_TYPES.SpreadElement\n ) {\n return;\n }\n\n mut_nodesToFix.push({\n node,\n objExpression: objArg,\n keyExpression: keyArg,\n });\n },\n\n // Handle key in obj\n BinaryExpression: (node) => {\n if (node.operator !== 'in') return;\n\n const keyExpression = node.left;\n\n const objExpression = node.right;\n\n // Type guard: ensure keyExpression is Expression (not PrivateIdentifier)\n if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) return;\n\n mut_nodesToFix.push({\n node,\n objExpression,\n keyExpression,\n });\n },\n\n 'Program:exit': () => {\n const namedImports = getNamedImports(tsDataForgeImport);\n\n const rewrites = mut_nodesToFix.map((entry) => ({\n ...entry,\n // A record-typed object makes the `isRecord(...)` conjunct always\n // true, so it is left out of the replacement entirely.\n needsIsRecord: !isKnownRecord(entry.objExpression),\n }));\n\n const importsToAdd: readonly string[] = [\n ...(rewrites.some(({ needsIsRecord }) => needsIsRecord) &&\n !namedImports.includes('isRecord')\n ? ['isRecord']\n : []),\n ...(namedImports.includes('hasKey') ? [] : ['hasKey']),\n ];\n\n for (const [\n index,\n { node, objExpression, keyExpression, needsIsRecord },\n ] of rewrites.entries()) {\n const objText = sourceCode.getText(objExpression);\n\n const keyText = sourceCode.getText(keyExpression);\n\n const originalText = sourceCode.getText(node);\n\n context.report({\n node,\n messageId: needsIsRecord ? 'useIsRecordAndHasKey' : 'useHasKey',\n data: {\n original: originalText,\n objName: objText,\n keyName: keyText,\n },\n fix: (fixer) => {\n const replacement = needsIsRecord\n ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`\n : `hasKey(${objText}, ${keyText})`;\n\n // All import fixes have to live on the first report:\n // `insertTextBefore(program, …)` produces overlapping ranges\n // otherwise and only one of them would survive a pass.\n const importFixes =\n index === 0 && importsToAdd.length > 0\n ? buildImportFixes(\n fixer,\n program,\n tsDataForgeImport,\n importsToAdd,\n )\n : [];\n\n return [...importFixes, fixer.replaceText(node, replacement)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n } as const;\n\n/**\n * Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`\n * constraint, so that guarding the call with `isRecord` adds nothing.\n *\n * `UnknownRecord` is `ReadonlyRecord<string, unknown>`, so a string index\n * signature is exactly what it asks for and its value type is `unknown`, which\n * anything satisfies. Callables and arrays are excluded because `isRecord`\n * rejects them at runtime (`typeof fn === 'object'` is false, `Array.isArray`\n * is true), so dropping the guard there would change behavior.\n *\n * Deliberately conservative: a type TypeScript would accept through an\n * *implicit* index signature (an object type alias without one of its own)\n * keeps the guard, because the checker does not report such a signature here.\n */\nconst isAlreadyRecordType = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n checker: ts.TypeChecker,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n type: ts.Type,\n): boolean =>\n type.isUnion()\n ? type.types.every((member) => isAlreadyRecordType(checker, member))\n : checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&\n Arr.isEmpty(checker.getSignaturesOfType(type, ts.SignatureKind.Call)) &&\n Arr.isEmpty(\n checker.getSignaturesOfType(type, ts.SignatureKind.Construct),\n ) &&\n !checker.isArrayType(type) &&\n !checker.isTupleType(type);\n"],"names":[],"mappings":"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"}
@@ -21,7 +21,7 @@ const preferRangeForLoop = {
21
21
  const mut_nodesToFix = [];
22
22
  return {
23
23
  ForStatement: (node) => {
24
- if (node.init?.type !== AST_NODE_TYPES.VariableDeclaration || node.init.kind !== "let" || !Arr.isFixedLengthTuple(node.init.declarations, 1)) {
24
+ if (node.init?.type !== AST_NODE_TYPES.VariableDeclaration || node.init.kind !== "let" || !Arr.isFixedLengthTuple(1, node.init.declarations)) {
25
25
  return;
26
26
  }
27
27
  const declaration = node.init.declarations[0];
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-range-for-loop.mjs","sources":["../../src/rules/prefer-range-for-loop.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useRangeForLoop';\n\nexport const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `for (let i = begin; i < end; ++i)` with `for (const i of range(begin, end))` from ts-data-forge.',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useRangeForLoop:\n 'Replace with `for (const {{varName}} of range({{begin}}, {{end}}))` from ts-data-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsDataForgeImport = getTsDataForgeImport(program);\n\n const mut_nodesToFix: {\n node: TSESTree.ForStatement;\n varName: string;\n begin: string;\n end: string;\n step?: string;\n }[] = [];\n\n return {\n ForStatement: (node) => {\n // Check init: let i = begin\n if (\n node.init?.type !== AST_NODE_TYPES.VariableDeclaration ||\n node.init.kind !== 'let' ||\n !Arr.isFixedLengthTuple(node.init.declarations, 1)\n ) {\n return;\n }\n\n const declaration = node.init.declarations[0];\n\n if (\n declaration.id.type !== AST_NODE_TYPES.Identifier ||\n declaration.init === null\n ) {\n return;\n }\n\n const varName = declaration.id.name;\n\n const beginExpr = declaration.init;\n\n // Check test: i < end\n if (\n node.test?.type !== AST_NODE_TYPES.BinaryExpression ||\n node.test.operator !== '<' ||\n node.test.left.type !== AST_NODE_TYPES.Identifier ||\n node.test.left.name !== varName\n ) {\n return;\n }\n\n const endExpr = node.test.right;\n\n // Check update: ++i, i++, i += 1, or i += step\n if (node.update === null) return;\n\n let mut_step: string | undefined;\n\n if (node.update.type === AST_NODE_TYPES.UpdateExpression) {\n // ++i or i++\n if (\n node.update.operator === '++' &&\n node.update.argument.type === AST_NODE_TYPES.Identifier &&\n node.update.argument.name === varName\n ) {\n mut_step = undefined; // step defaults to 1\n } else {\n return;\n }\n } else if (\n node.update.type === AST_NODE_TYPES.AssignmentExpression &&\n node.update.operator === '+=' &&\n node.update.left.type === AST_NODE_TYPES.Identifier &&\n node.update.left.name === varName\n ) {\n // i += step\n const stepValue = node.update.right;\n\n if (stepValue.type === AST_NODE_TYPES.Literal) {\n const stepNum = stepValue.value;\n\n // Only support positive integer steps\n if (typeof stepNum === 'number' && stepNum > 0) {\n mut_step = stepNum === 1 ? undefined : String(stepNum);\n } else {\n return;\n }\n } else {\n // Variable step like i += step\n mut_step = sourceCode.getText(stepValue);\n }\n } else {\n return;\n }\n\n const beginText = sourceCode.getText(beginExpr);\n\n const endText = sourceCode.getText(endExpr);\n\n mut_nodesToFix.push({\n node,\n varName,\n begin: beginText,\n end: endText,\n step: mut_step,\n });\n },\n 'Program:exit': () => {\n const namedImports = getNamedImports(tsDataForgeImport);\n\n const hasRangeImport = namedImports.includes('range');\n\n for (const [\n index,\n { node, varName, begin, end, step },\n ] of mut_nodesToFix.entries()) {\n context.report({\n node: node.init ?? node,\n messageId: 'useRangeForLoop',\n data: {\n varName,\n begin,\n end,\n },\n fix: (fixer) => {\n const bodyText = sourceCode.getText(node.body);\n\n const rangeArgs =\n step === undefined\n ? `${begin}, ${end}`\n : `${begin}, ${end}, ${step}`;\n\n const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;\n\n const importFixes =\n index === 0 && !hasRangeImport\n ? buildImportFixes(fixer, program, tsDataForgeImport, [\n 'range',\n ])\n : [];\n\n return [...importFixes, fixer.replaceText(node, replacement)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n} as const;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prefer-range-for-loop.mjs","sources":["../../src/rules/prefer-range-for-loop.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useRangeForLoop';\n\nexport const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `for (let i = begin; i < end; ++i)` with `for (const i of range(begin, end))` from ts-data-forge.',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useRangeForLoop:\n 'Replace with `for (const {{varName}} of range({{begin}}, {{end}}))` from ts-data-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsDataForgeImport = getTsDataForgeImport(program);\n\n const mut_nodesToFix: {\n node: TSESTree.ForStatement;\n varName: string;\n begin: string;\n end: string;\n step?: string;\n }[] = [];\n\n return {\n ForStatement: (node) => {\n // Check init: let i = begin\n if (\n node.init?.type !== AST_NODE_TYPES.VariableDeclaration ||\n node.init.kind !== 'let' ||\n !Arr.isFixedLengthTuple(1, node.init.declarations)\n ) {\n return;\n }\n\n const declaration = node.init.declarations[0];\n\n if (\n declaration.id.type !== AST_NODE_TYPES.Identifier ||\n declaration.init === null\n ) {\n return;\n }\n\n const varName = declaration.id.name;\n\n const beginExpr = declaration.init;\n\n // Check test: i < end\n if (\n node.test?.type !== AST_NODE_TYPES.BinaryExpression ||\n node.test.operator !== '<' ||\n node.test.left.type !== AST_NODE_TYPES.Identifier ||\n node.test.left.name !== varName\n ) {\n return;\n }\n\n const endExpr = node.test.right;\n\n // Check update: ++i, i++, i += 1, or i += step\n if (node.update === null) return;\n\n let mut_step: string | undefined;\n\n if (node.update.type === AST_NODE_TYPES.UpdateExpression) {\n // ++i or i++\n if (\n node.update.operator === '++' &&\n node.update.argument.type === AST_NODE_TYPES.Identifier &&\n node.update.argument.name === varName\n ) {\n mut_step = undefined; // step defaults to 1\n } else {\n return;\n }\n } else if (\n node.update.type === AST_NODE_TYPES.AssignmentExpression &&\n node.update.operator === '+=' &&\n node.update.left.type === AST_NODE_TYPES.Identifier &&\n node.update.left.name === varName\n ) {\n // i += step\n const stepValue = node.update.right;\n\n if (stepValue.type === AST_NODE_TYPES.Literal) {\n const stepNum = stepValue.value;\n\n // Only support positive integer steps\n if (typeof stepNum === 'number' && stepNum > 0) {\n mut_step = stepNum === 1 ? undefined : String(stepNum);\n } else {\n return;\n }\n } else {\n // Variable step like i += step\n mut_step = sourceCode.getText(stepValue);\n }\n } else {\n return;\n }\n\n const beginText = sourceCode.getText(beginExpr);\n\n const endText = sourceCode.getText(endExpr);\n\n mut_nodesToFix.push({\n node,\n varName,\n begin: beginText,\n end: endText,\n step: mut_step,\n });\n },\n 'Program:exit': () => {\n const namedImports = getNamedImports(tsDataForgeImport);\n\n const hasRangeImport = namedImports.includes('range');\n\n for (const [\n index,\n { node, varName, begin, end, step },\n ] of mut_nodesToFix.entries()) {\n context.report({\n node: node.init ?? node,\n messageId: 'useRangeForLoop',\n data: {\n varName,\n begin,\n end,\n },\n fix: (fixer) => {\n const bodyText = sourceCode.getText(node.body);\n\n const rangeArgs =\n step === undefined\n ? `${begin}, ${end}`\n : `${begin}, ${end}, ${step}`;\n\n const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;\n\n const importFixes =\n index === 0 && !hasRangeImport\n ? buildImportFixes(fixer, program, tsDataForgeImport, [\n 'range',\n ])\n : [];\n\n return [...importFixes, fixer.replaceText(node, replacement)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n} as const;\n"],"names":[],"mappings":"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"}
@@ -1,12 +1,13 @@
1
1
  export declare const tsDataForgeRules: {
2
2
  readonly 'prefer-canonical-array-slicing': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferCanonicalArraySlicing", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
3
3
  readonly 'prefer-canonical-length-guard': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useCanonicalGuard" | "useIsBoundedLengthArray" | "useIsFixedLengthArray" | "useIsMaxLengthArray" | "useIsMinLengthArray" | "useIsNonEmpty", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
4
+ readonly 'prefer-canonical-length-cast': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useCanonicalCast", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
4
5
  readonly 'prefer-arr-is-array': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useArrIsArray", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
5
6
  readonly 'prefer-arr-sum': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useArrSum" | "useArrSumBy", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
7
  readonly 'prefer-as-int': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useBrandedNumberCastFunction", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
7
8
  readonly 'prefer-is-non-null-object': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNonNullObject", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
8
9
  readonly 'prefer-range-for-loop': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useRangeForLoop", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
9
- readonly 'prefer-is-record-and-has-key': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsRecordAndHasKey", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
10
+ readonly 'prefer-is-record-and-has-key': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useHasKey" | "useIsRecordAndHasKey", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
10
11
  readonly 'prefer-num-safe-parse-int': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useSafeParseInt", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
11
12
  readonly 'prefer-num-safe-parse-float': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useSafeParseFloat", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
12
13
  readonly 'no-unnecessary-type-guard': import("@typescript-eslint/utils/ts-eslint").RuleModule<"alwaysFalse" | "alwaysTrue" | "replaceTypeGuard", readonly [(Readonly<{
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAcA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;CAaa,CAAC"}
1
+ {"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAeA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;CAca,CAAC"}
@@ -3,6 +3,7 @@ import { preferArrIsArray } from './prefer-arr-is-array.mjs';
3
3
  import { preferArrSum } from './prefer-arr-sum.mjs';
4
4
  import { preferAsInt } from './prefer-as-int.mjs';
5
5
  import { preferCanonicalArraySlicing } from './prefer-canonical-array-slicing.mjs';
6
+ import { preferCanonicalLengthCast } from './prefer-canonical-length-cast.mjs';
6
7
  import { preferCanonicalLengthGuard } from './prefer-canonical-length-guard.mjs';
7
8
  import { preferComparisonOverNullishGuard } from './prefer-comparison-over-nullish-guard.mjs';
8
9
  import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
@@ -14,6 +15,7 @@ import { preferRangeForLoop } from './prefer-range-for-loop.mjs';
14
15
  const tsDataForgeRules = {
15
16
  "prefer-canonical-array-slicing": preferCanonicalArraySlicing,
16
17
  "prefer-canonical-length-guard": preferCanonicalLengthGuard,
18
+ "prefer-canonical-length-cast": preferCanonicalLengthCast,
17
19
  "prefer-arr-is-array": preferArrIsArray,
18
20
  "prefer-arr-sum": preferArrSum,
19
21
  "prefer-as-int": preferAsInt,
@@ -1 +1 @@
1
- {"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { noUnnecessaryTypeGuard } from './no-unnecessary-type-guard.mjs';\nimport { preferArrIsArray } from './prefer-arr-is-array.mjs';\nimport { preferArrSum } from './prefer-arr-sum.mjs';\nimport { preferAsInt } from './prefer-as-int.mjs';\nimport { preferCanonicalArraySlicing } from './prefer-canonical-array-slicing.mjs';\nimport { preferCanonicalLengthGuard } from './prefer-canonical-length-guard.mjs';\nimport { preferComparisonOverNullishGuard } from './prefer-comparison-over-nullish-guard.mjs';\nimport { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';\nimport { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';\nimport { preferNumSafeParseFloat } from './prefer-num-safe-parse-float.mjs';\nimport { preferNumSafeParseInt } from './prefer-num-safe-parse-int.mjs';\nimport { preferRangeForLoop } from './prefer-range-for-loop.mjs';\n\nexport const tsDataForgeRules = {\n 'prefer-canonical-array-slicing': preferCanonicalArraySlicing,\n 'prefer-canonical-length-guard': preferCanonicalLengthGuard,\n 'prefer-arr-is-array': preferArrIsArray,\n 'prefer-arr-sum': preferArrSum,\n 'prefer-as-int': preferAsInt,\n 'prefer-is-non-null-object': preferIsNonNullObject,\n 'prefer-range-for-loop': preferRangeForLoop,\n 'prefer-is-record-and-has-key': preferIsRecordAndHasKey,\n 'prefer-num-safe-parse-int': preferNumSafeParseInt,\n 'prefer-num-safe-parse-float': preferNumSafeParseFloat,\n 'no-unnecessary-type-guard': noUnnecessaryTypeGuard,\n 'prefer-comparison-over-nullish-guard': preferComparisonOverNullishGuard,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;;;;;;;;;;;;AAcO,MAAM,gBAAA,GAAmB;AAAA,EAC9B,gCAAA,EAAkC,2BAAA;AAAA,EAClC,+BAAA,EAAiC,0BAAA;AAAA,EACjC,qBAAA,EAAuB,gBAAA;AAAA,EACvB,gBAAA,EAAkB,YAAA;AAAA,EAClB,eAAA,EAAiB,WAAA;AAAA,EACjB,2BAAA,EAA6B,qBAAA;AAAA,EAC7B,uBAAA,EAAyB,kBAAA;AAAA,EACzB,8BAAA,EAAgC,uBAAA;AAAA,EAChC,2BAAA,EAA6B,qBAAA;AAAA,EAC7B,6BAAA,EAA+B,uBAAA;AAAA,EAC/B,2BAAA,EAA6B,sBAAA;AAAA,EAC7B,sCAAA,EAAwC;AAC1C;;;;"}
1
+ {"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { noUnnecessaryTypeGuard } from './no-unnecessary-type-guard.mjs';\nimport { preferArrIsArray } from './prefer-arr-is-array.mjs';\nimport { preferArrSum } from './prefer-arr-sum.mjs';\nimport { preferAsInt } from './prefer-as-int.mjs';\nimport { preferCanonicalArraySlicing } from './prefer-canonical-array-slicing.mjs';\nimport { preferCanonicalLengthCast } from './prefer-canonical-length-cast.mjs';\nimport { preferCanonicalLengthGuard } from './prefer-canonical-length-guard.mjs';\nimport { preferComparisonOverNullishGuard } from './prefer-comparison-over-nullish-guard.mjs';\nimport { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';\nimport { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';\nimport { preferNumSafeParseFloat } from './prefer-num-safe-parse-float.mjs';\nimport { preferNumSafeParseInt } from './prefer-num-safe-parse-int.mjs';\nimport { preferRangeForLoop } from './prefer-range-for-loop.mjs';\n\nexport const tsDataForgeRules = {\n 'prefer-canonical-array-slicing': preferCanonicalArraySlicing,\n 'prefer-canonical-length-guard': preferCanonicalLengthGuard,\n 'prefer-canonical-length-cast': preferCanonicalLengthCast,\n 'prefer-arr-is-array': preferArrIsArray,\n 'prefer-arr-sum': preferArrSum,\n 'prefer-as-int': preferAsInt,\n 'prefer-is-non-null-object': preferIsNonNullObject,\n 'prefer-range-for-loop': preferRangeForLoop,\n 'prefer-is-record-and-has-key': preferIsRecordAndHasKey,\n 'prefer-num-safe-parse-int': preferNumSafeParseInt,\n 'prefer-num-safe-parse-float': preferNumSafeParseFloat,\n 'no-unnecessary-type-guard': noUnnecessaryTypeGuard,\n 'prefer-comparison-over-nullish-guard': preferComparisonOverNullishGuard,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAeO,MAAM,gBAAA,GAAmB;AAAA,EAC9B,gCAAA,EAAkC,2BAAA;AAAA,EAClC,+BAAA,EAAiC,0BAAA;AAAA,EACjC,8BAAA,EAAgC,yBAAA;AAAA,EAChC,qBAAA,EAAuB,gBAAA;AAAA,EACvB,gBAAA,EAAkB,YAAA;AAAA,EAClB,eAAA,EAAiB,WAAA;AAAA,EACjB,2BAAA,EAA6B,qBAAA;AAAA,EAC7B,uBAAA,EAAyB,kBAAA;AAAA,EACzB,8BAAA,EAAgC,uBAAA;AAAA,EAChC,2BAAA,EAA6B,qBAAA;AAAA,EAC7B,6BAAA,EAA+B,uBAAA;AAAA,EAC/B,2BAAA,EAA6B,sBAAA;AAAA,EAC7B,sCAAA,EAAwC;AAC1C;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-ts-data-forge",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "ESLint rules that steer code toward ts-data-forge idioms.",
6
6
  "keywords": [
@@ -34,13 +34,13 @@
34
34
  "LICENSE"
35
35
  ],
36
36
  "dependencies": {
37
- "@typescript-eslint/utils": "8.64.0",
38
- "ts-type-forge": "~7.2.1",
39
- "ts-data-forge": "13.0.0"
37
+ "@typescript-eslint/utils": "^8.65.0",
38
+ "ts-type-forge": "~9.1.1",
39
+ "ts-data-forge": "14.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@typescript-eslint/parser": "8.64.0",
43
- "@typescript-eslint/rule-tester": "8.64.0",
42
+ "@typescript-eslint/parser": "8.65.0",
43
+ "@typescript-eslint/rule-tester": "8.65.0",
44
44
  "dedent": "1.7.2"
45
45
  },
46
46
  "peerDependencies": {
package/src/plugin.mts CHANGED
@@ -9,6 +9,7 @@ import { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';
9
9
  */
10
10
  const recommendedRules = {
11
11
  'ts-data-forge/prefer-canonical-array-slicing': 'error',
12
+ 'ts-data-forge/prefer-canonical-length-cast': 'error',
12
13
  'ts-data-forge/prefer-canonical-length-guard': 'error',
13
14
  'ts-data-forge/prefer-arr-is-array': 'error',
14
15
  'ts-data-forge/prefer-arr-sum': 'error',
@@ -43,7 +43,7 @@ namespace PreferCanonicalArraySlicing {
43
43
  }
44
44
 
45
45
  /**
46
- * @description Normalize array-length checks to their canonical `Arr` guard: `xs.length <op> n` becomes the matching `Arr.is*` guard, and degenerate guards (e.g. `Arr.isFixedLengthTuple(xs, 0)`) become `Arr.isEmpty` / `Arr.isNonEmpty`.
46
+ * @description Normalize array-length checks to their canonical `Arr` guard: `xs.length <op> n` becomes the matching `Arr.is*` guard, and a degenerate guard becomes the named one for its family (`Arr.isFixedLengthArray(0, xs)` → `Arr.isEmpty`, `Arr.isFixedLengthTuple(0, xs)` → `Arr.isEmptyTuple`).
47
47
  *
48
48
  * ```md
49
49
  * | key | value |
@@ -57,6 +57,21 @@ namespace PreferCanonicalLengthGuard {
57
57
  export type RuleEntry = Linter.StringSeverity;
58
58
  }
59
59
 
60
+ /**
61
+ * @description Normalize an `Arr` length cast whose bounds are degenerate (e.g. `Arr.asMinLengthArray(1, xs)`, `Arr.asFixedLengthTuple(0, xs)`) to the cast that names that constraint directly, staying within the branded `*Array` or structural `*Tuple` family.
62
+ *
63
+ * ```md
64
+ * | key | value |
65
+ * | :--------- | :--------- |
66
+ * | type | suggestion |
67
+ * | deprecated | false |
68
+ * | fixable | code |
69
+ * ```
70
+ */
71
+ namespace PreferCanonicalLengthCast {
72
+ export type RuleEntry = Linter.StringSeverity;
73
+ }
74
+
60
75
  /**
61
76
  * @description Replace `Array.isArray` with `Arr.isArray` from ts-data-forge.
62
77
  *
@@ -238,6 +253,7 @@ namespace PreferComparisonOverNullishGuard {
238
253
  export type EslintTsDataForgeRules = Readonly<{
239
254
  'ts-data-forge/prefer-canonical-array-slicing': PreferCanonicalArraySlicing.RuleEntry;
240
255
  'ts-data-forge/prefer-canonical-length-guard': PreferCanonicalLengthGuard.RuleEntry;
256
+ 'ts-data-forge/prefer-canonical-length-cast': PreferCanonicalLengthCast.RuleEntry;
241
257
  'ts-data-forge/prefer-arr-is-array': PreferArrIsArray.RuleEntry;
242
258
  'ts-data-forge/prefer-arr-sum': PreferArrSum.RuleEntry;
243
259
  'ts-data-forge/prefer-as-int': PreferAsInt.RuleEntry;
@@ -149,4 +149,3 @@ export const buildImportFixes = (
149
149
  // (organize-imports will handle merging/deduplication)
150
150
  return [fixer.insertTextBefore(program, insertionText)];
151
151
  };
152
- /* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */
@@ -44,6 +44,7 @@ type GuardSpec = DeepReadonly<
44
44
  replacements?: Record<string, string>;
45
45
  }
46
46
  | { kind: 'nonEmptyString' }
47
+ | { kind: 'record' }
47
48
  >;
48
49
 
49
50
  /**
@@ -98,6 +99,7 @@ const GUARD_SPECS: DeepReadonly<Record<string, GuardSpec>> = {
98
99
  replacements: NULLISH_REPLACEMENTS_EXCLUDE,
99
100
  },
100
101
  isNonEmptyString: { kind: 'nonEmptyString' },
102
+ isRecord: { kind: 'record' },
101
103
  } as const;
102
104
 
103
105
  const DEFERRED_OR_OPAQUE_FLAGS =
@@ -341,6 +343,20 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
341
343
  break;
342
344
  }
343
345
 
346
+ case 'record': {
347
+ if (parts.every((p) => isGuaranteedRecord(p, checker))) {
348
+ // Every member already satisfies `UnknownRecord`.
349
+ reportConstant(node, canonicalName, true, argument);
350
+ } else if (
351
+ parts.every((p) => isGuaranteedNotRecord(p, checker))
352
+ ) {
353
+ // No member can ever be a non-null, non-array object.
354
+ reportConstant(node, canonicalName, false, argument);
355
+ }
356
+
357
+ break;
358
+ }
359
+
344
360
  case 'nonEmptyString': {
345
361
  const hasNullish =
346
362
  inputAtoms.has('undefined') || inputAtoms.has('null');
@@ -403,6 +419,55 @@ const collectUnionParts = (type: ts.Type): readonly ts.Type[] | undefined => {
403
419
  return parts;
404
420
  };
405
421
 
422
+ /**
423
+ * Whether `isRecord` is guaranteed to accept this (non-union) type.
424
+ *
425
+ * `isRecord` narrows to `UnknownRecord` (`ReadonlyRecord<string, unknown>`), so
426
+ * a string index signature is exactly what it asks for and its value type is
427
+ * `unknown`, which anything satisfies. Arrays and callables are excluded even
428
+ * when they carry one, because `isRecord` rejects them at runtime
429
+ * (`Array.isArray` is true; `typeof fn === 'object'` is false).
430
+ *
431
+ * Deliberately conservative: an object type without an index signature of its
432
+ * own is reported as neither guaranteed-accepted nor guaranteed-rejected, even
433
+ * where TypeScript would accept it through an implicit index signature.
434
+ */
435
+ const isGuaranteedRecord = (
436
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
437
+ type: ts.Type,
438
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
439
+ checker: ts.TypeChecker,
440
+ ): boolean =>
441
+ checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&
442
+ !isCallableType(type, checker) &&
443
+ !checker.isArrayType(type) &&
444
+ !checker.isTupleType(type);
445
+
446
+ /**
447
+ * Whether `isRecord` is guaranteed to reject this (non-union) type: every
448
+ * primitive (`null` included), every array or tuple, and every callable fails
449
+ * its `typeof x === 'object' && x !== null && !Array.isArray(x)` check.
450
+ */
451
+ const isGuaranteedNotRecord = (
452
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
453
+ type: ts.Type,
454
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
455
+ checker: ts.TypeChecker,
456
+ ): boolean =>
457
+ classifyAtom(type) !== 'other' ||
458
+ checker.isArrayType(type) ||
459
+ checker.isTupleType(type) ||
460
+ isCallableType(type, checker);
461
+
462
+ const isCallableType = (
463
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
464
+ type: ts.Type,
465
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
466
+ checker: ts.TypeChecker,
467
+ ): boolean =>
468
+ checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0 ||
469
+ checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0;
470
+
406
471
  /** Classifies a single (non-union) type into a primitive {@link Atom}. */
407
472
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
408
473
  const classifyAtom = (type: ts.Type): Atom => {
@@ -20,6 +20,30 @@ const tester = new RuleTester({
20
20
  describe('no-unnecessary-type-guard', () => {
21
21
  tester.run('no-unnecessary-type-guard', noUnnecessaryTypeGuard, {
22
22
  valid: [
23
+ {
24
+ name: 'isRecord that actually narrows a union',
25
+ code: dedent`
26
+ import { isRecord } from 'ts-data-forge';
27
+ declare const x: Record<string, unknown> | string;
28
+ const y = isRecord(x);
29
+ `,
30
+ },
31
+ {
32
+ name: 'isRecord on an object type without an index signature of its own',
33
+ code: dedent`
34
+ import { isRecord } from 'ts-data-forge';
35
+ declare const x: Readonly<{ a: number }>;
36
+ const y = isRecord(x);
37
+ `,
38
+ },
39
+ {
40
+ name: 'isRecord on unknown',
41
+ code: dedent`
42
+ import { isRecord } from 'ts-data-forge';
43
+ declare const x: unknown;
44
+ const y = isRecord(x);
45
+ `,
46
+ },
23
47
  {
24
48
  name: 'isNotUndefined on a type that includes undefined',
25
49
  code: dedent`
@@ -110,6 +134,76 @@ describe('no-unnecessary-type-guard', () => {
110
134
  },
111
135
  ],
112
136
  invalid: [
137
+ {
138
+ name: 'isRecord on a record type (always true)',
139
+ code: dedent`
140
+ import { isRecord } from 'ts-data-forge';
141
+ declare const x: Readonly<Record<string, unknown>>;
142
+ const y = isRecord(x);
143
+ `,
144
+ output: dedent`
145
+ import { isRecord } from 'ts-data-forge';
146
+ declare const x: Readonly<Record<string, unknown>>;
147
+ const y = true;
148
+ `,
149
+ errors: [{ messageId: 'alwaysTrue' }],
150
+ },
151
+ {
152
+ name: 'isRecord on a union of record types (always true)',
153
+ code: dedent`
154
+ import { isRecord } from 'ts-data-forge';
155
+ declare const x: Record<string, number> | Record<string, string>;
156
+ const y = isRecord(x);
157
+ `,
158
+ output: dedent`
159
+ import { isRecord } from 'ts-data-forge';
160
+ declare const x: Record<string, number> | Record<string, string>;
161
+ const y = true;
162
+ `,
163
+ errors: [{ messageId: 'alwaysTrue' }],
164
+ },
165
+ {
166
+ name: 'isRecord on a primitive (always false)',
167
+ code: dedent`
168
+ import { isRecord } from 'ts-data-forge';
169
+ declare const x: string | null;
170
+ const y = isRecord(x);
171
+ `,
172
+ output: dedent`
173
+ import { isRecord } from 'ts-data-forge';
174
+ declare const x: string | null;
175
+ const y = false;
176
+ `,
177
+ errors: [{ messageId: 'alwaysFalse' }],
178
+ },
179
+ {
180
+ name: 'isRecord on an array (always false)',
181
+ code: dedent`
182
+ import { isRecord } from 'ts-data-forge';
183
+ declare const x: readonly number[];
184
+ const y = isRecord(x);
185
+ `,
186
+ output: dedent`
187
+ import { isRecord } from 'ts-data-forge';
188
+ declare const x: readonly number[];
189
+ const y = false;
190
+ `,
191
+ errors: [{ messageId: 'alwaysFalse' }],
192
+ },
193
+ {
194
+ name: 'isRecord on a function (always false)',
195
+ code: dedent`
196
+ import { isRecord } from 'ts-data-forge';
197
+ declare const x: () => void;
198
+ const y = isRecord(x);
199
+ `,
200
+ output: dedent`
201
+ import { isRecord } from 'ts-data-forge';
202
+ declare const x: () => void;
203
+ const y = false;
204
+ `,
205
+ errors: [{ messageId: 'alwaysFalse' }],
206
+ },
113
207
  {
114
208
  name: 'isNotUndefined on a type that cannot be undefined (always true)',
115
209
  code: dedent`
@@ -125,7 +125,7 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
125
125
  max: maxText,
126
126
  },
127
127
  fix: (fixer) => {
128
- const replacement = `Arr.isBoundedLengthArray(${arrayText}, ${minText}, ${maxText})`;
128
+ const replacement = `Arr.isBoundedLengthArray(${minText}, ${maxText}, ${arrayText})`;
129
129
 
130
130
  const importFixes =
131
131
  index === 0 && !hasArrImport