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.
- package/README.md +16 -14
- package/dist/plugin.d.mts +3 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +16 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +13 -1
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-sum.mjs +2 -2
- package/dist/rules/prefer-arr-sum.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.d.mts +6 -0
- package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -0
- package/dist/rules/prefer-canonical-length-cast.mjs +155 -0
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -0
- package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.mjs +12 -8
- package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +30 -16
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +1 -1
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +2 -1
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +6 -6
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/import-utils.mts +0 -1
- package/src/rules/no-unnecessary-type-guard.mts +65 -0
- package/src/rules/no-unnecessary-type-guard.test.mts +94 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +1 -1
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +5 -5
- package/src/rules/prefer-arr-is-fixed-length-array.mts +1 -1
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +11 -11
- package/src/rules/prefer-arr-is-max-length-array.mts +1 -1
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -8
- package/src/rules/prefer-arr-is-min-length-array.mts +1 -1
- package/src/rules/prefer-arr-is-min-length-array.test.mts +9 -9
- package/src/rules/prefer-arr-sum.mts +2 -2
- package/src/rules/prefer-canonical-array-slicing.mts +0 -2
- package/src/rules/prefer-canonical-length-cast.mts +290 -0
- package/src/rules/prefer-canonical-length-cast.test.mts +305 -0
- package/src/rules/prefer-canonical-length-guard.mts +40 -21
- package/src/rules/prefer-canonical-length-guard.test.mts +32 -32
- package/src/rules/prefer-is-record-and-has-key.mts +84 -20
- package/src/rules/prefer-is-record-and-has-key.test.mts +74 -0
- package/src/rules/prefer-range-for-loop.mts +1 -1
- 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;
|
|
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
|
|
55
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
{"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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"}
|
package/dist/rules/rules.d.mts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAeA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;CAca,CAAC"}
|
package/dist/rules/rules.mjs
CHANGED
|
@@ -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,
|
package/dist/rules/rules.mjs.map
CHANGED
|
@@ -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":"
|
|
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.
|
|
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.
|
|
38
|
-
"ts-type-forge": "~
|
|
39
|
-
"ts-data-forge": "
|
|
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.
|
|
43
|
-
"@typescript-eslint/rule-tester": "8.
|
|
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',
|
package/src/rule-types.mts
CHANGED
|
@@ -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
|
|
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;
|
|
@@ -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(${
|
|
128
|
+
const replacement = `Arr.isBoundedLengthArray(${minText}, ${maxText}, ${arrayText})`;
|
|
129
129
|
|
|
130
130
|
const importFixes =
|
|
131
131
|
index === 0 && !hasArrImport
|