eslint-plugin-ts-data-forge 0.1.0 → 0.1.2

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 (105) hide show
  1. package/dist/index.d.mts +5 -0
  2. package/dist/index.d.mts.map +1 -0
  3. package/dist/index.mjs +3 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/dist/plugin.d.mts +3 -0
  6. package/dist/plugin.d.mts.map +1 -0
  7. package/dist/plugin.mjs +11 -0
  8. package/dist/plugin.mjs.map +1 -0
  9. package/dist/rule-types.d.mts +279 -0
  10. package/dist/rule-types.d.mts.map +1 -0
  11. package/dist/rule-types.mjs +2 -0
  12. package/dist/rule-types.mjs.map +1 -0
  13. package/dist/rules/ast-utils.d.mts +29 -0
  14. package/dist/rules/ast-utils.d.mts.map +1 -0
  15. package/dist/rules/ast-utils.mjs +54 -0
  16. package/dist/rules/ast-utils.mjs.map +1 -0
  17. package/dist/rules/branded-number-types.d.mts +5 -0
  18. package/dist/rules/branded-number-types.d.mts.map +1 -0
  19. package/dist/rules/branded-number-types.mjs +40 -0
  20. package/dist/rules/branded-number-types.mjs.map +1 -0
  21. package/dist/rules/constants.d.mts +2 -0
  22. package/dist/rules/constants.d.mts.map +1 -0
  23. package/dist/rules/constants.mjs +4 -0
  24. package/dist/rules/constants.mjs.map +1 -0
  25. package/dist/rules/import-utils.d.mts +30 -0
  26. package/dist/rules/import-utils.d.mts.map +1 -0
  27. package/dist/rules/import-utils.mjs +61 -0
  28. package/dist/rules/import-utils.mjs.map +1 -0
  29. package/dist/rules/index.d.mts +2 -0
  30. package/dist/rules/index.d.mts.map +1 -0
  31. package/dist/rules/index.mjs +2 -0
  32. package/dist/rules/index.mjs.map +1 -0
  33. package/dist/rules/no-unnecessary-type-guard.d.mts +11 -0
  34. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -0
  35. package/dist/rules/no-unnecessary-type-guard.mjs +278 -0
  36. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -0
  37. package/dist/rules/prefer-arr-is-array.d.mts +6 -0
  38. package/dist/rules/prefer-arr-is-array.d.mts.map +1 -0
  39. package/dist/rules/prefer-arr-is-array.mjs +57 -0
  40. package/dist/rules/prefer-arr-is-array.mjs.map +1 -0
  41. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +6 -0
  42. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -0
  43. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +89 -0
  44. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -0
  45. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +6 -0
  46. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -0
  47. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +87 -0
  48. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -0
  49. package/dist/rules/prefer-arr-is-max-length-array.d.mts +6 -0
  50. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -0
  51. package/dist/rules/prefer-arr-is-max-length-array.mjs +83 -0
  52. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -0
  53. package/dist/rules/prefer-arr-is-min-length-array.d.mts +6 -0
  54. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -0
  55. package/dist/rules/prefer-arr-is-min-length-array.mjs +83 -0
  56. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -0
  57. package/dist/rules/prefer-arr-is-non-empty.d.mts +6 -0
  58. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -0
  59. package/dist/rules/prefer-arr-is-non-empty.mjs +80 -0
  60. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -0
  61. package/dist/rules/prefer-arr-sum.d.mts +6 -0
  62. package/dist/rules/prefer-arr-sum.d.mts.map +1 -0
  63. package/dist/rules/prefer-arr-sum.mjs +136 -0
  64. package/dist/rules/prefer-arr-sum.mjs.map +1 -0
  65. package/dist/rules/prefer-as-int.d.mts +6 -0
  66. package/dist/rules/prefer-as-int.d.mts.map +1 -0
  67. package/dist/rules/prefer-as-int.mjs +77 -0
  68. package/dist/rules/prefer-as-int.mjs.map +1 -0
  69. package/dist/rules/prefer-canonical-array-slicing.d.mts +6 -0
  70. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -0
  71. package/dist/rules/prefer-canonical-array-slicing.mjs +362 -0
  72. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -0
  73. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts +6 -0
  74. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -0
  75. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +80 -0
  76. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -0
  77. package/dist/rules/prefer-is-non-null-object.d.mts +6 -0
  78. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -0
  79. package/dist/rules/prefer-is-non-null-object.mjs +81 -0
  80. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -0
  81. package/dist/rules/prefer-is-record-and-has-key.d.mts +6 -0
  82. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -0
  83. package/dist/rules/prefer-is-record-and-has-key.mjs +97 -0
  84. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -0
  85. package/dist/rules/prefer-num-safe-parse-float.d.mts +14 -0
  86. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -0
  87. package/dist/rules/prefer-num-safe-parse-float.mjs +84 -0
  88. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -0
  89. package/dist/rules/prefer-num-safe-parse-int.d.mts +14 -0
  90. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -0
  91. package/dist/rules/prefer-num-safe-parse-int.mjs +88 -0
  92. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -0
  93. package/dist/rules/prefer-range-for-loop.d.mts +6 -0
  94. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -0
  95. package/dist/rules/prefer-range-for-loop.mjs +103 -0
  96. package/dist/rules/prefer-range-for-loop.mjs.map +1 -0
  97. package/dist/rules/rules.d.mts +21 -0
  98. package/dist/rules/rules.d.mts.map +1 -0
  99. package/dist/rules/rules.mjs +38 -0
  100. package/dist/rules/rules.mjs.map +1 -0
  101. package/dist/types.d.mts +4 -0
  102. package/dist/types.d.mts.map +1 -0
  103. package/dist/types.mjs +2 -0
  104. package/dist/types.mjs.map +1 -0
  105. package/package.json +20 -20
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-comparison-over-nullish-guard.mjs","sources":["../../src/rules/prefer-comparison-over-nullish-guard.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { type DeepReadonly } from 'ts-type-forge';\nimport { buildCalleeResolver, getTsDataForgeImport } from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'preferComparison';\n\ntype ComparisonSpec = Readonly<{\n op: '!==' | '===';\n literal: 'null' | 'undefined';\n}>;\n\n/**\n * The nullish guards that are meant for point-free use (e.g.\n * `xs.filter(isNotUndefined)`). When called with an explicit argument a direct\n * comparison is clearer, so map each to its equivalent operator and literal.\n */\nconst COMPARISON_GUARDS: DeepReadonly<Record<string, ComparisonSpec>> = {\n isUndefined: { op: '===', literal: 'undefined' },\n isNotUndefined: { op: '!==', literal: 'undefined' },\n isNull: { op: '===', literal: 'null' },\n isNotNull: { op: '!==', literal: 'null' },\n} as const;\n\n/**\n * Argument expression kinds whose precedence is lower than equality, so they\n * must be parenthesized when placed on the left of `===` / `!==`.\n */\nconst ARG_NEEDS_PARENS: ReadonlySet<string> = new Set([\n AST_NODE_TYPES.LogicalExpression,\n AST_NODE_TYPES.ConditionalExpression,\n AST_NODE_TYPES.AssignmentExpression,\n AST_NODE_TYPES.SequenceExpression,\n AST_NODE_TYPES.ArrowFunctionExpression,\n AST_NODE_TYPES.YieldExpression,\n]);\n\n/**\n * Returns `true` when the comparison must be wrapped in parentheses to preserve\n * the original grouping given the call expression's parent context.\n */\nconst needsWrappingInParent = (\n node: DeepReadonly<TSESTree.CallExpression>,\n): boolean => {\n const { parent } = node;\n\n // `parent.type` is one of 130+ AST kinds; only a handful need wrapping.\n // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check\n switch (parent.type) {\n case AST_NODE_TYPES.UnaryExpression:\n case AST_NODE_TYPES.AwaitExpression:\n case AST_NODE_TYPES.BinaryExpression:\n case AST_NODE_TYPES.TSNonNullExpression:\n return true;\n\n case AST_NODE_TYPES.MemberExpression:\n return parent.object === node;\n\n case AST_NODE_TYPES.CallExpression:\n case AST_NODE_TYPES.NewExpression:\n return parent.callee === node;\n\n default:\n return false;\n }\n};\n\nexport const preferComparisonOverNullishGuard: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer a direct `=== null` / `!== undefined` comparison over calling `isNull`, `isNotNull`, `isUndefined`, or `isNotUndefined` with an explicit argument (those guards are intended for point-free use such as `xs.filter(isNotUndefined)`).',\n },\n fixable: 'code',\n schema: [],\n messages: {\n preferComparison:\n 'Prefer `{{comparison}}` over calling `{{name}}` with an explicit argument.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const resolveGuard = buildCalleeResolver(\n getTsDataForgeImport(sourceCode.ast),\n );\n\n return {\n CallExpression: (node) => {\n if (node.arguments.length !== 1) return;\n\n const argument = node.arguments[0];\n\n if (\n argument === undefined ||\n argument.type === AST_NODE_TYPES.SpreadElement\n ) {\n return;\n }\n\n const resolved = resolveGuard(node.callee);\n\n if (resolved === undefined) return;\n\n const spec = COMPARISON_GUARDS[resolved.canonicalName];\n\n if (spec === undefined) return;\n\n const argRaw = sourceCode.getText(argument);\n\n const argText = ARG_NEEDS_PARENS.has(argument.type)\n ? `(${argRaw})`\n : argRaw;\n\n const comparison = `${argText} ${spec.op} ${spec.literal}`;\n\n const replacement = needsWrappingInParent(node)\n ? `(${comparison})`\n : comparison;\n\n context.report({\n node,\n messageId: 'preferComparison',\n data: { comparison, name: resolved.canonicalName },\n fix: (fixer) => fixer.replaceText(node, replacement),\n });\n },\n };\n },\n defaultOptions: [],\n} as const;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ type Options = readonly [];
3
+ type MessageIds = 'useIsNonNullObject';
4
+ export declare const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options>;
5
+ export {};
6
+ //# sourceMappingURL=prefer-is-non-null-object.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-is-non-null-object.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAQlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,oBAAoB,CAAC;AAEvC,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CAsEjE,CAAC"}
@@ -0,0 +1,81 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { getTsDataForgeImport, getNamedImports, buildImportFixes } from './import-utils.mjs';
3
+
4
+ const preferIsNonNullObject = {
5
+ meta: {
6
+ type: "suggestion",
7
+ docs: {
8
+ description: 'Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-data-forge.'
9
+ },
10
+ fixable: "code",
11
+ schema: [],
12
+ messages: {
13
+ useIsNonNullObject: "Replace the object/null check with `isNonNullObject()` from ts-data-forge."
14
+ }
15
+ },
16
+ create: (context) => {
17
+ const sourceCode = context.sourceCode;
18
+ const program = sourceCode.ast;
19
+ const tsDataForgeImport = getTsDataForgeImport(program);
20
+ const mut_nodesToFix = [];
21
+ return {
22
+ LogicalExpression: (node) => {
23
+ const identifierName = getNonNullObjectIdentifierName(node);
24
+ if (identifierName === void 0) return;
25
+ mut_nodesToFix.push({ node, identifierName });
26
+ },
27
+ "Program:exit": () => {
28
+ const hasIsNonNullObjectImport = getNamedImports(tsDataForgeImport).includes("isNonNullObject");
29
+ for (const [
30
+ index,
31
+ { node, identifierName }
32
+ ] of mut_nodesToFix.entries()) {
33
+ context.report({
34
+ node,
35
+ messageId: "useIsNonNullObject",
36
+ fix: (fixer) => {
37
+ const replacement = `isNonNullObject(${identifierName})`;
38
+ const importFixes = index === 0 && !hasIsNonNullObjectImport ? buildImportFixes(fixer, program, tsDataForgeImport, [
39
+ "isNonNullObject"
40
+ ]) : [];
41
+ return [...importFixes, fixer.replaceText(node, replacement)];
42
+ }
43
+ });
44
+ }
45
+ }
46
+ };
47
+ },
48
+ defaultOptions: []
49
+ };
50
+ const getNonNullObjectIdentifierName = (node) => {
51
+ if (node.operator !== "&&") return void 0;
52
+ const leftIdentifierName = getTypeofObjectIdentifierName(node.left);
53
+ const rightIdentifierName = getNonNullCheckIdentifierName(node.right);
54
+ if (leftIdentifierName === void 0) return void 0;
55
+ if (rightIdentifierName === void 0) return void 0;
56
+ if (leftIdentifierName !== rightIdentifierName) return void 0;
57
+ return leftIdentifierName;
58
+ };
59
+ const getTypeofObjectIdentifierName = (node) => {
60
+ if (node.type !== AST_NODE_TYPES.BinaryExpression) return void 0;
61
+ if (node.operator !== "===") return void 0;
62
+ const { left, right } = node;
63
+ if (left.type !== AST_NODE_TYPES.UnaryExpression) return void 0;
64
+ if (left.operator !== "typeof") return void 0;
65
+ if (right.type !== AST_NODE_TYPES.Literal) return void 0;
66
+ if (right.value !== "object") return void 0;
67
+ if (left.argument.type !== AST_NODE_TYPES.Identifier) return void 0;
68
+ return left.argument.name;
69
+ };
70
+ const getNonNullCheckIdentifierName = (node) => {
71
+ if (node.type !== AST_NODE_TYPES.BinaryExpression) return void 0;
72
+ if (node.operator !== "!==") return void 0;
73
+ const { left, right } = node;
74
+ if (left.type !== AST_NODE_TYPES.Identifier) return void 0;
75
+ if (right.type !== AST_NODE_TYPES.Literal) return void 0;
76
+ if (right.value !== null) return void 0;
77
+ return left.name;
78
+ };
79
+
80
+ export { preferIsNonNullObject };
81
+ //# sourceMappingURL=prefer-is-non-null-object.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-is-non-null-object.mjs","sources":["../../src/rules/prefer-is-non-null-object.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { type DeepReadonly } from 'ts-type-forge';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useIsNonNullObject';\n\nexport const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `typeof u === \"object\" && u !== null` with `isNonNullObject(u)` from ts-data-forge.',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useIsNonNullObject:\n 'Replace the object/null check with `isNonNullObject()` 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.LogicalExpression;\n identifierName: string;\n }[] = [];\n\n return {\n LogicalExpression: (node) => {\n const identifierName = getNonNullObjectIdentifierName(node);\n\n if (identifierName === undefined) return;\n\n mut_nodesToFix.push({ node, identifierName });\n },\n 'Program:exit': () => {\n // Check if isNonNullObject is already imported\n const hasIsNonNullObjectImport =\n getNamedImports(tsDataForgeImport).includes('isNonNullObject');\n\n // Note: We add import only for the first node to avoid conflicts when\n // multiple fixes try to insert at the same position. This means if the\n // first node is disabled via eslint-disable comment, no import will be\n // added.\n for (const [\n index,\n { node, identifierName },\n ] of mut_nodesToFix.entries()) {\n context.report({\n node,\n messageId: 'useIsNonNullObject',\n fix: (fixer) => {\n const replacement = `isNonNullObject(${identifierName})`;\n\n // Add import only for the first node and only if not already imported\n const importFixes =\n index === 0 && !hasIsNonNullObjectImport\n ? buildImportFixes(fixer, program, tsDataForgeImport, [\n 'isNonNullObject',\n ])\n : [];\n\n return [...importFixes, fixer.replaceText(node, replacement)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n} as const;\n\nconst getNonNullObjectIdentifierName = (\n node: DeepReadonly<TSESTree.LogicalExpression>,\n): string | undefined => {\n if (node.operator !== '&&') return undefined;\n\n const leftIdentifierName = getTypeofObjectIdentifierName(node.left);\n\n const rightIdentifierName = getNonNullCheckIdentifierName(node.right);\n\n if (leftIdentifierName === undefined) return undefined;\n\n if (rightIdentifierName === undefined) return undefined;\n\n if (leftIdentifierName !== rightIdentifierName) return undefined;\n\n return leftIdentifierName;\n};\n\nconst getTypeofObjectIdentifierName = (\n node: DeepReadonly<TSESTree.Expression>,\n): string | undefined => {\n if (node.type !== AST_NODE_TYPES.BinaryExpression) return undefined;\n\n if (node.operator !== '===') return undefined;\n\n const { left, right } = node;\n\n if (left.type !== AST_NODE_TYPES.UnaryExpression) return undefined;\n\n if (left.operator !== 'typeof') return undefined;\n\n if (right.type !== AST_NODE_TYPES.Literal) return undefined;\n\n if (right.value !== 'object') return undefined;\n\n if (left.argument.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return left.argument.name;\n};\n\nconst getNonNullCheckIdentifierName = (\n node: DeepReadonly<TSESTree.Expression>,\n): string | undefined => {\n if (node.type !== AST_NODE_TYPES.BinaryExpression) return undefined;\n\n if (node.operator !== '!==') return undefined;\n\n const { left, right } = node;\n\n if (left.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n if (right.type !== AST_NODE_TYPES.Literal) return undefined;\n\n if (right.value !== null) return undefined;\n\n return left.name;\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ type Options = readonly [];
3
+ type MessageIds = 'useIsRecordAndHasKey';
4
+ export declare const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options>;
5
+ export {};
6
+ //# sourceMappingURL=prefer-is-record-and-has-key.d.mts.map
@@ -0,0 +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"}
@@ -0,0 +1,97 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { getTsDataForgeImport, getNamedImports, buildImportFixes } from './import-utils.mjs';
3
+
4
+ const preferIsRecordAndHasKey = {
5
+ meta: {
6
+ type: "suggestion",
7
+ docs: {
8
+ description: "Replace `Object.hasOwn(obj, key)` or `key in obj` with `isRecord(obj) && hasKey(obj, key)` from ts-data-forge"
9
+ },
10
+ fixable: "code",
11
+ schema: [],
12
+ messages: {
13
+ useIsRecordAndHasKey: "Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge."
14
+ }
15
+ },
16
+ create: (context) => {
17
+ const sourceCode = context.sourceCode;
18
+ const program = sourceCode.ast;
19
+ const tsDataForgeImport = getTsDataForgeImport(program);
20
+ const mut_nodesToFix = [];
21
+ return {
22
+ // Handle Object.hasOwn(obj, key)
23
+ CallExpression: (node) => {
24
+ if (node.callee.type !== AST_NODE_TYPES.MemberExpression || node.callee.object.type !== AST_NODE_TYPES.Identifier || node.callee.object.name !== "Object" || node.callee.property.type !== AST_NODE_TYPES.Identifier || node.callee.property.name !== "hasOwn") {
25
+ return;
26
+ }
27
+ if (node.arguments.length !== 2) return;
28
+ const objArg = node.arguments[0];
29
+ const keyArg = node.arguments[1];
30
+ if (objArg === void 0 || keyArg === void 0) return;
31
+ if (objArg.type === AST_NODE_TYPES.SpreadElement || keyArg.type === AST_NODE_TYPES.SpreadElement) {
32
+ return;
33
+ }
34
+ mut_nodesToFix.push({
35
+ node,
36
+ objExpression: objArg,
37
+ keyExpression: keyArg
38
+ });
39
+ },
40
+ // Handle key in obj
41
+ BinaryExpression: (node) => {
42
+ if (node.operator !== "in") return;
43
+ const keyExpression = node.left;
44
+ const objExpression = node.right;
45
+ if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) return;
46
+ mut_nodesToFix.push({
47
+ node,
48
+ objExpression,
49
+ keyExpression
50
+ });
51
+ },
52
+ "Program:exit": () => {
53
+ const namedImports = getNamedImports(tsDataForgeImport);
54
+ const hasIsRecordImport = namedImports.includes("isRecord");
55
+ const hasHasKeyImport = namedImports.includes("hasKey");
56
+ for (const [
57
+ index,
58
+ { node, objExpression, keyExpression }
59
+ ] of mut_nodesToFix.entries()) {
60
+ const objText = sourceCode.getText(objExpression);
61
+ const keyText = sourceCode.getText(keyExpression);
62
+ const originalText = sourceCode.getText(node);
63
+ context.report({
64
+ node,
65
+ messageId: "useIsRecordAndHasKey",
66
+ data: {
67
+ original: originalText,
68
+ objName: objText,
69
+ keyName: keyText
70
+ },
71
+ 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(
81
+ fixer,
82
+ program,
83
+ tsDataForgeImport,
84
+ mut_importsToAdd
85
+ ) : [];
86
+ return [...importFixes, fixer.replaceText(node, replacement)];
87
+ }
88
+ });
89
+ }
90
+ }
91
+ };
92
+ },
93
+ defaultOptions: []
94
+ };
95
+
96
+ export { preferIsRecordAndHasKey };
97
+ //# sourceMappingURL=prefer-is-record-and-has-key.mjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,14 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ type Options = readonly [];
3
+ type MessageIds = 'useSafeParseFloat';
4
+ /**
5
+ * Detects `parseFloat(x)`, `Number.parseFloat(x)`, and `Number(x)` (when x
6
+ * is typed as `string`) and replaces them with
7
+ * `Result.unwrapOkOr(Num.safeParseFloat(x), Number.NaN)` (ts-data-forge).
8
+ *
9
+ * `Number(x)` is only flagged when x is purely a string type to avoid false
10
+ * positives for `Number(someBoolean)` or `Number(someNumber)` uses.
11
+ */
12
+ export declare const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options>;
13
+ export {};
14
+ //# sourceMappingURL=prefer-num-safe-parse-float.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-num-safe-parse-float.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-num-safe-parse-float.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAQlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,mBAAmB,CAAC;AAkBtC;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA6HjE,CAAC"}
@@ -0,0 +1,84 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import * as ts from 'typescript';
3
+ import { getTsDataForgeImport, getNamedImports, buildImportFixes } from './import-utils.mjs';
4
+
5
+ const isStringType = (type) => {
6
+ const parts = type.isUnion() ? type.types : [type];
7
+ return parts.length > 0 && parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0);
8
+ };
9
+ const preferNumSafeParseFloat = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description: "Replace `parseFloat(x)`, `Number.parseFloat(x)`, or `Number(x)` (when x is a string) with `Result.unwrapOkOr(Num.safeParseFloat(x), Number.NaN)` from ts-data-forge."
14
+ },
15
+ fixable: "code",
16
+ schema: [],
17
+ messages: {
18
+ useSafeParseFloat: "Replace `{{original}}` with `Result.unwrapOkOr(Num.safeParseFloat({{argName}}), Number.NaN)` from ts-data-forge."
19
+ }
20
+ },
21
+ create: (context) => {
22
+ const sourceCode = context.sourceCode;
23
+ const program = sourceCode.ast;
24
+ const tsDataForgeImport = getTsDataForgeImport(program);
25
+ const services = context.sourceCode.parserServices;
26
+ const mut_nodesToFix = [];
27
+ return {
28
+ CallExpression: (node) => {
29
+ const { callee } = node;
30
+ const isGlobalParseFloat = callee.type === AST_NODE_TYPES.Identifier && callee.name === "parseFloat";
31
+ const isNumberParseFloat = callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES.Identifier && callee.object.name === "Number" && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "parseFloat";
32
+ const isNumberCall = callee.type === AST_NODE_TYPES.Identifier && callee.name === "Number";
33
+ if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall)
34
+ return;
35
+ const args = node.arguments;
36
+ const firstArg = args[0];
37
+ if (firstArg === void 0) return;
38
+ if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;
39
+ if (services?.program == null) return;
40
+ const checker = services.program.getTypeChecker();
41
+ const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
42
+ if (tsNode === void 0) return;
43
+ const argType = checker.getTypeAtLocation(tsNode);
44
+ if (!isStringType(argType)) return;
45
+ mut_nodesToFix.push({ node, argExpression: firstArg });
46
+ },
47
+ "Program:exit": () => {
48
+ const namedImports = getNamedImports(tsDataForgeImport);
49
+ const missingImports = ["Num", "Result"].filter(
50
+ (name) => !namedImports.includes(name)
51
+ );
52
+ for (const [
53
+ index,
54
+ { node, argExpression }
55
+ ] of mut_nodesToFix.entries()) {
56
+ const argText = sourceCode.getText(argExpression);
57
+ const originalText = sourceCode.getText(node);
58
+ context.report({
59
+ node,
60
+ messageId: "useSafeParseFloat",
61
+ data: {
62
+ original: originalText,
63
+ argName: argText
64
+ },
65
+ fix: (fixer) => {
66
+ const core = `Result.unwrapOkOr(Num.safeParseFloat(${argText}), Number.NaN)`;
67
+ const importFixes = index === 0 && missingImports.length > 0 ? buildImportFixes(
68
+ fixer,
69
+ program,
70
+ tsDataForgeImport,
71
+ missingImports
72
+ ) : [];
73
+ return [...importFixes, fixer.replaceText(node, core)];
74
+ }
75
+ });
76
+ }
77
+ }
78
+ };
79
+ },
80
+ defaultOptions: []
81
+ };
82
+
83
+ export { preferNumSafeParseFloat };
84
+ //# sourceMappingURL=prefer-num-safe-parse-float.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-num-safe-parse-float.mjs","sources":["../../src/rules/prefer-num-safe-parse-float.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport * as ts from 'typescript';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useSafeParseFloat';\n\n/**\n * Returns `true` when every constituent of `type` is string-like, i.e. the\n * type is assignable to the `string` parameter of `Num.safeParseFloat`. A\n * union such as `string | undefined` is rejected so the autofix never\n * produces a type error.\n */\n// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\nconst isStringType = (type: ts.Type): boolean => {\n const parts = type.isUnion() ? type.types : ([type] as const);\n\n return (\n parts.length > 0 &&\n parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)\n );\n};\n\n/**\n * Detects `parseFloat(x)`, `Number.parseFloat(x)`, and `Number(x)` (when x\n * is typed as `string`) and replaces them with\n * `Result.unwrapOkOr(Num.safeParseFloat(x), Number.NaN)` (ts-data-forge).\n *\n * `Number(x)` is only flagged when x is purely a string type to avoid false\n * positives for `Number(someBoolean)` or `Number(someNumber)` uses.\n */\nexport const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =\n {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `parseFloat(x)`, `Number.parseFloat(x)`, or `Number(x)` (when x is a string) with `Result.unwrapOkOr(Num.safeParseFloat(x), Number.NaN)` from ts-data-forge.',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useSafeParseFloat:\n 'Replace `{{original}}` with `Result.unwrapOkOr(Num.safeParseFloat({{argName}}), Number.NaN)` 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 = context.sourceCode.parserServices;\n\n const mut_nodesToFix: {\n node: TSESTree.CallExpression;\n argExpression: TSESTree.Expression;\n }[] = [];\n\n return {\n CallExpression: (node) => {\n const { callee } = node;\n\n // Match `parseFloat(...)` (global function).\n const isGlobalParseFloat =\n callee.type === AST_NODE_TYPES.Identifier &&\n callee.name === 'parseFloat';\n\n // Match `Number.parseFloat(...)`.\n const isNumberParseFloat =\n callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.object.type === AST_NODE_TYPES.Identifier &&\n callee.object.name === 'Number' &&\n callee.property.type === AST_NODE_TYPES.Identifier &&\n callee.property.name === 'parseFloat';\n\n // Match `Number(...)` called as a function (not `new Number(...)`).\n const isNumberCall =\n callee.type === AST_NODE_TYPES.Identifier &&\n callee.name === 'Number';\n\n if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall)\n return;\n\n const args = node.arguments;\n\n const firstArg = args[0];\n\n if (firstArg === undefined) return;\n\n // Spread argument (`parseFloat(...rest)`) cannot be rewritten safely.\n if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;\n\n // The argument must be purely `string` so the autofix is type-safe\n // against `Num.safeParseFloat(s: string)`. Without type information,\n // skip.\n if (services?.program == null) return;\n\n const checker = services.program.getTypeChecker();\n\n const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);\n\n if (tsNode === undefined) return;\n\n const argType = checker.getTypeAtLocation(tsNode);\n\n if (!isStringType(argType)) return;\n\n mut_nodesToFix.push({ node, argExpression: firstArg });\n },\n 'Program:exit': () => {\n const namedImports = getNamedImports(tsDataForgeImport);\n\n const missingImports = (['Num', 'Result'] as const).filter(\n (name) => !namedImports.includes(name),\n );\n\n for (const [\n index,\n { node, argExpression },\n ] of mut_nodesToFix.entries()) {\n const argText = sourceCode.getText(argExpression);\n\n const originalText = sourceCode.getText(node);\n\n context.report({\n node,\n messageId: 'useSafeParseFloat',\n data: {\n original: originalText,\n argName: argText,\n },\n fix: (fixer) => {\n const core = `Result.unwrapOkOr(Num.safeParseFloat(${argText}), Number.NaN)`;\n\n const importFixes =\n index === 0 && missingImports.length > 0\n ? buildImportFixes(\n fixer,\n program,\n tsDataForgeImport,\n missingImports,\n )\n : [];\n\n return [...importFixes, fixer.replaceText(node, core)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n } as const;\n"],"names":[],"mappings":";;;;AAuBA,MAAM,YAAA,GAAe,CAAC,IAAA,KAA2B;AAC/C,EAAA,MAAM,QAAQ,IAAA,CAAK,OAAA,KAAY,IAAA,CAAK,KAAA,GAAS,CAAC,IAAI,CAAA;AAElD,EAAA,OACE,KAAA,CAAM,MAAA,GAAS,CAAA,IACf,KAAA,CAAM,KAAA,CAAM,CAAC,CAAA,KAAA,CAAO,CAAA,CAAE,KAAA,GAAQ,EAAA,CAAG,SAAA,CAAU,UAAA,MAAgB,CAAC,CAAA;AAEhE,CAAA;AAUO,MAAM,uBAAA,GACX;AAAA,EACE,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,MACR,iBAAA,EACE;AAAA;AACJ,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,IAAA,MAAM,UAAU,UAAA,CAAW,GAAA;AAE3B,IAAA,MAAM,iBAAA,GAAoB,qBAAqB,OAAO,CAAA;AAEtD,IAAA,MAAM,QAAA,GAAW,QAAQ,UAAA,CAAW,cAAA;AAEpC,IAAA,MAAM,iBAGA,EAAC;AAEP,IAAA,OAAO;AAAA,MACL,cAAA,EAAgB,CAAC,IAAA,KAAS;AACxB,QAAA,MAAM,EAAE,QAAO,GAAI,IAAA;AAGnB,QAAA,MAAM,qBACJ,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,UAAA,IAC/B,OAAO,IAAA,KAAS,YAAA;AAGlB,QAAA,MAAM,kBAAA,GACJ,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,gBAAA,IAC/B,CAAC,MAAA,CAAO,QAAA,IACR,MAAA,CAAO,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,cACtC,MAAA,CAAO,MAAA,CAAO,IAAA,KAAS,QAAA,IACvB,MAAA,CAAO,QAAA,CAAS,SAAS,cAAA,CAAe,UAAA,IACxC,MAAA,CAAO,QAAA,CAAS,IAAA,KAAS,YAAA;AAG3B,QAAA,MAAM,eACJ,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,UAAA,IAC/B,OAAO,IAAA,KAAS,QAAA;AAElB,QAAA,IAAI,CAAC,kBAAA,IAAsB,CAAC,kBAAA,IAAsB,CAAC,YAAA;AACjD,UAAA;AAEF,QAAA,MAAM,OAAO,IAAA,CAAK,SAAA;AAElB,QAAA,MAAM,QAAA,GAAW,KAAK,CAAC,CAAA;AAEvB,QAAA,IAAI,aAAa,MAAA,EAAW;AAG5B,QAAA,IAAI,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,aAAA,EAAe;AAKpD,QAAA,IAAI,QAAA,EAAU,WAAW,IAAA,EAAM;AAE/B,QAAA,MAAM,OAAA,GAAU,QAAA,CAAS,OAAA,CAAQ,cAAA,EAAe;AAEhD,QAAA,MAAM,MAAA,GAAS,QAAA,CAAS,qBAAA,EAAuB,GAAA,CAAI,QAAQ,CAAA;AAE3D,QAAA,IAAI,WAAW,MAAA,EAAW;AAE1B,QAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,iBAAA,CAAkB,MAAM,CAAA;AAEhD,QAAA,IAAI,CAAC,YAAA,CAAa,OAAO,CAAA,EAAG;AAE5B,QAAA,cAAA,CAAe,IAAA,CAAK,EAAE,IAAA,EAAM,aAAA,EAAe,UAAU,CAAA;AAAA,MACvD,CAAA;AAAA,MACA,gBAAgB,MAAM;AACpB,QAAA,MAAM,YAAA,GAAe,gBAAgB,iBAAiB,CAAA;AAEtD,QAAA,MAAM,cAAA,GAAkB,CAAC,KAAA,EAAO,QAAQ,CAAA,CAAY,MAAA;AAAA,UAClD,CAAC,IAAA,KAAS,CAAC,YAAA,CAAa,SAAS,IAAI;AAAA,SACvC;AAEA,QAAA,KAAA,MAAW;AAAA,UACT,KAAA;AAAA,UACA,EAAE,MAAM,aAAA;AAAc,SACxB,IAAK,cAAA,CAAe,OAAA,EAAQ,EAAG;AAC7B,UAAA,MAAM,OAAA,GAAU,UAAA,CAAW,OAAA,CAAQ,aAAa,CAAA;AAEhD,UAAA,MAAM,YAAA,GAAe,UAAA,CAAW,OAAA,CAAQ,IAAI,CAAA;AAE5C,UAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,YACb,IAAA;AAAA,YACA,SAAA,EAAW,mBAAA;AAAA,YACX,IAAA,EAAM;AAAA,cACJ,QAAA,EAAU,YAAA;AAAA,cACV,OAAA,EAAS;AAAA,aACX;AAAA,YACA,GAAA,EAAK,CAAC,KAAA,KAAU;AACd,cAAA,MAAM,IAAA,GAAO,wCAAwC,OAAO,CAAA,cAAA,CAAA;AAE5D,cAAA,MAAM,WAAA,GACJ,KAAA,KAAU,CAAA,IAAK,cAAA,CAAe,SAAS,CAAA,GACnC,gBAAA;AAAA,gBACE,KAAA;AAAA,gBACA,OAAA;AAAA,gBACA,iBAAA;AAAA,gBACA;AAAA,kBAEF,EAAC;AAEP,cAAA,OAAO,CAAC,GAAG,WAAA,EAAa,MAAM,WAAA,CAAY,IAAA,EAAM,IAAI,CAAC,CAAA;AAAA,YACvD;AAAA,WACD,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,gBAAgB;AAClB;;;;"}
@@ -0,0 +1,14 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ type Options = readonly [];
3
+ type MessageIds = 'useSafeParseInt';
4
+ /**
5
+ * Detects the base-10 forms of `parseInt` / `Number.parseInt` (`parseInt(x)` or
6
+ * `parseInt(x, 10)`) and replaces them with
7
+ * `Result.unwrapOkOr(Num.safeParseInt(x), Number.NaN)` (ts-data-forge).
8
+ *
9
+ * Other radixes (e.g. `parseInt(x, 16)`) are intentionally left untouched, as
10
+ * `Num.safeParseInt` only supports base 10.
11
+ */
12
+ export declare const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options>;
13
+ export {};
14
+ //# sourceMappingURL=prefer-num-safe-parse-int.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-num-safe-parse-int.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-num-safe-parse-int.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAQlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,iBAAiB,CAAC;AAkBpC;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA6HjE,CAAC"}
@@ -0,0 +1,88 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import * as ts from 'typescript';
3
+ import { getTsDataForgeImport, getNamedImports, buildImportFixes } from './import-utils.mjs';
4
+
5
+ const isStringType = (type) => {
6
+ const parts = type.isUnion() ? type.types : [type];
7
+ return parts.length > 0 && parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0);
8
+ };
9
+ const preferNumSafeParseInt = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description: "Replace `parseInt(x, 10)` with `Result.unwrapOkOr(Num.safeParseInt(x), Number.NaN)` from ts-data-forge."
14
+ },
15
+ fixable: "code",
16
+ schema: [],
17
+ messages: {
18
+ useSafeParseInt: "Replace `{{original}}` with `Result.unwrapOkOr(Num.safeParseInt({{argName}}), Number.NaN)` from ts-data-forge."
19
+ }
20
+ },
21
+ create: (context) => {
22
+ const sourceCode = context.sourceCode;
23
+ const program = sourceCode.ast;
24
+ const tsDataForgeImport = getTsDataForgeImport(program);
25
+ const services = context.sourceCode.parserServices;
26
+ const mut_nodesToFix = [];
27
+ return {
28
+ CallExpression: (node) => {
29
+ const { callee } = node;
30
+ const isGlobalParseInt = callee.type === AST_NODE_TYPES.Identifier && callee.name === "parseInt";
31
+ const isNumberParseInt = callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES.Identifier && callee.object.name === "Number" && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "parseInt";
32
+ if (!isGlobalParseInt && !isNumberParseInt) return;
33
+ const args = node.arguments;
34
+ const firstArg = args[0];
35
+ if (firstArg === void 0) return;
36
+ if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;
37
+ if (args.length >= 2) {
38
+ const radix = args[1];
39
+ if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
40
+ return;
41
+ }
42
+ }
43
+ if (services?.program == null) return;
44
+ const checker = services.program.getTypeChecker();
45
+ const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
46
+ if (tsNode === void 0) return;
47
+ const argType = checker.getTypeAtLocation(tsNode);
48
+ if (!isStringType(argType)) return;
49
+ mut_nodesToFix.push({ node, argExpression: firstArg });
50
+ },
51
+ "Program:exit": () => {
52
+ const namedImports = getNamedImports(tsDataForgeImport);
53
+ const missingImports = ["Num", "Result"].filter(
54
+ (name) => !namedImports.includes(name)
55
+ );
56
+ for (const [
57
+ index,
58
+ { node, argExpression }
59
+ ] of mut_nodesToFix.entries()) {
60
+ const argText = sourceCode.getText(argExpression);
61
+ const originalText = sourceCode.getText(node);
62
+ context.report({
63
+ node,
64
+ messageId: "useSafeParseInt",
65
+ data: {
66
+ original: originalText,
67
+ argName: argText
68
+ },
69
+ fix: (fixer) => {
70
+ const core = `Result.unwrapOkOr(Num.safeParseInt(${argText}), Number.NaN)`;
71
+ const importFixes = index === 0 && missingImports.length > 0 ? buildImportFixes(
72
+ fixer,
73
+ program,
74
+ tsDataForgeImport,
75
+ missingImports
76
+ ) : [];
77
+ return [...importFixes, fixer.replaceText(node, core)];
78
+ }
79
+ });
80
+ }
81
+ }
82
+ };
83
+ },
84
+ defaultOptions: []
85
+ };
86
+
87
+ export { preferNumSafeParseInt };
88
+ //# sourceMappingURL=prefer-num-safe-parse-int.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-num-safe-parse-int.mjs","sources":["../../src/rules/prefer-num-safe-parse-int.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport * as ts from 'typescript';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useSafeParseInt';\n\n/**\n * Returns `true` when every constituent of `type` is string-like, i.e. the type\n * is assignable to the `string` parameter of `Num.safeParseInt`. A union such\n * as `string | undefined` is rejected so the autofix never produces a type\n * error.\n */\n// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\nconst isStringType = (type: ts.Type): boolean => {\n const parts = type.isUnion() ? type.types : ([type] as const);\n\n return (\n parts.length > 0 &&\n parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)\n );\n};\n\n/**\n * Detects the base-10 forms of `parseInt` / `Number.parseInt` (`parseInt(x)` or\n * `parseInt(x, 10)`) and replaces them with\n * `Result.unwrapOkOr(Num.safeParseInt(x), Number.NaN)` (ts-data-forge).\n *\n * Other radixes (e.g. `parseInt(x, 16)`) are intentionally left untouched, as\n * `Num.safeParseInt` only supports base 10.\n */\nexport const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `parseInt(x, 10)` with `Result.unwrapOkOr(Num.safeParseInt(x), Number.NaN)` from ts-data-forge.',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useSafeParseInt:\n 'Replace `{{original}}` with `Result.unwrapOkOr(Num.safeParseInt({{argName}}), Number.NaN)` 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 = context.sourceCode.parserServices;\n\n const mut_nodesToFix: {\n node: TSESTree.CallExpression;\n argExpression: TSESTree.Expression;\n }[] = [];\n\n return {\n CallExpression: (node) => {\n const { callee } = node;\n\n // Match `parseInt(...)` or `Number.parseInt(...)`.\n const isGlobalParseInt =\n callee.type === AST_NODE_TYPES.Identifier &&\n callee.name === 'parseInt';\n\n const isNumberParseInt =\n callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.object.type === AST_NODE_TYPES.Identifier &&\n callee.object.name === 'Number' &&\n callee.property.type === AST_NODE_TYPES.Identifier &&\n callee.property.name === 'parseInt';\n\n if (!isGlobalParseInt && !isNumberParseInt) return;\n\n const args = node.arguments;\n\n const firstArg = args[0];\n\n if (firstArg === undefined) return;\n\n // Spread argument (`parseInt(...rest)`) cannot be rewritten safely.\n if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;\n\n // Only base 10: accept a missing radix or an explicit literal `10`.\n if (args.length >= 2) {\n const radix = args[1];\n\n if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {\n return;\n }\n }\n\n // The argument must be a `string` so the autofix is type-safe against\n // `Num.safeParseInt(s: string)`. Without type information, skip.\n if (services?.program == null) return;\n\n const checker = services.program.getTypeChecker();\n\n const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);\n\n if (tsNode === undefined) return;\n\n const argType = checker.getTypeAtLocation(tsNode);\n\n if (!isStringType(argType)) return;\n\n mut_nodesToFix.push({ node, argExpression: firstArg });\n },\n 'Program:exit': () => {\n const namedImports = getNamedImports(tsDataForgeImport);\n\n const missingImports = (['Num', 'Result'] as const).filter(\n (name) => !namedImports.includes(name),\n );\n\n for (const [\n index,\n { node, argExpression },\n ] of mut_nodesToFix.entries()) {\n const argText = sourceCode.getText(argExpression);\n\n const originalText = sourceCode.getText(node);\n\n context.report({\n node,\n messageId: 'useSafeParseInt',\n data: {\n original: originalText,\n argName: argText,\n },\n fix: (fixer) => {\n const core = `Result.unwrapOkOr(Num.safeParseInt(${argText}), Number.NaN)`;\n\n const importFixes =\n index === 0 && missingImports.length > 0\n ? buildImportFixes(\n fixer,\n program,\n tsDataForgeImport,\n missingImports,\n )\n : [];\n\n return [...importFixes, fixer.replaceText(node, core)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n} as const;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ type Options = readonly [];
3
+ type MessageIds = 'useRangeForLoop';
4
+ export declare const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options>;
5
+ export {};
6
+ //# sourceMappingURL=prefer-range-for-loop.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-range-for-loop.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-range-for-loop.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAQlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,iBAAiB,CAAC;AAEpC,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CAkK9D,CAAC"}
@@ -0,0 +1,103 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { Arr } from 'ts-data-forge';
3
+ import { getTsDataForgeImport, getNamedImports, buildImportFixes } from './import-utils.mjs';
4
+
5
+ const preferRangeForLoop = {
6
+ meta: {
7
+ type: "suggestion",
8
+ docs: {
9
+ description: "Replace `for (let i = begin; i < end; ++i)` with `for (const i of range(begin, end))` from ts-data-forge."
10
+ },
11
+ fixable: "code",
12
+ schema: [],
13
+ messages: {
14
+ useRangeForLoop: "Replace with `for (const {{varName}} of range({{begin}}, {{end}}))` from ts-data-forge."
15
+ }
16
+ },
17
+ create: (context) => {
18
+ const sourceCode = context.sourceCode;
19
+ const program = sourceCode.ast;
20
+ const tsDataForgeImport = getTsDataForgeImport(program);
21
+ const mut_nodesToFix = [];
22
+ return {
23
+ ForStatement: (node) => {
24
+ if (node.init?.type !== AST_NODE_TYPES.VariableDeclaration || node.init.kind !== "let" || !Arr.isFixedLengthTuple(node.init.declarations, 1)) {
25
+ return;
26
+ }
27
+ const declaration = node.init.declarations[0];
28
+ if (declaration.id.type !== AST_NODE_TYPES.Identifier || declaration.init === null) {
29
+ return;
30
+ }
31
+ const varName = declaration.id.name;
32
+ const beginExpr = declaration.init;
33
+ if (node.test?.type !== AST_NODE_TYPES.BinaryExpression || node.test.operator !== "<" || node.test.left.type !== AST_NODE_TYPES.Identifier || node.test.left.name !== varName) {
34
+ return;
35
+ }
36
+ const endExpr = node.test.right;
37
+ if (node.update === null) return;
38
+ let mut_step;
39
+ if (node.update.type === AST_NODE_TYPES.UpdateExpression) {
40
+ if (node.update.operator === "++" && node.update.argument.type === AST_NODE_TYPES.Identifier && node.update.argument.name === varName) {
41
+ mut_step = void 0;
42
+ } else {
43
+ return;
44
+ }
45
+ } else if (node.update.type === AST_NODE_TYPES.AssignmentExpression && node.update.operator === "+=" && node.update.left.type === AST_NODE_TYPES.Identifier && node.update.left.name === varName) {
46
+ const stepValue = node.update.right;
47
+ if (stepValue.type === AST_NODE_TYPES.Literal) {
48
+ const stepNum = stepValue.value;
49
+ if (typeof stepNum === "number" && stepNum > 0) {
50
+ mut_step = stepNum === 1 ? void 0 : String(stepNum);
51
+ } else {
52
+ return;
53
+ }
54
+ } else {
55
+ mut_step = sourceCode.getText(stepValue);
56
+ }
57
+ } else {
58
+ return;
59
+ }
60
+ const beginText = sourceCode.getText(beginExpr);
61
+ const endText = sourceCode.getText(endExpr);
62
+ mut_nodesToFix.push({
63
+ node,
64
+ varName,
65
+ begin: beginText,
66
+ end: endText,
67
+ step: mut_step
68
+ });
69
+ },
70
+ "Program:exit": () => {
71
+ const namedImports = getNamedImports(tsDataForgeImport);
72
+ const hasRangeImport = namedImports.includes("range");
73
+ for (const [
74
+ index,
75
+ { node, varName, begin, end, step }
76
+ ] of mut_nodesToFix.entries()) {
77
+ context.report({
78
+ node: node.init ?? node,
79
+ messageId: "useRangeForLoop",
80
+ data: {
81
+ varName,
82
+ begin,
83
+ end
84
+ },
85
+ fix: (fixer) => {
86
+ const bodyText = sourceCode.getText(node.body);
87
+ const rangeArgs = step === void 0 ? `${begin}, ${end}` : `${begin}, ${end}, ${step}`;
88
+ const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;
89
+ const importFixes = index === 0 && !hasRangeImport ? buildImportFixes(fixer, program, tsDataForgeImport, [
90
+ "range"
91
+ ]) : [];
92
+ return [...importFixes, fixer.replaceText(node, replacement)];
93
+ }
94
+ });
95
+ }
96
+ }
97
+ };
98
+ },
99
+ defaultOptions: []
100
+ };
101
+
102
+ export { preferRangeForLoop };
103
+ //# sourceMappingURL=prefer-range-for-loop.mjs.map