eslint-plugin-ts-data-forge 0.1.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/LICENSE +201 -0
- package/README.md +57 -0
- package/package.json +74 -0
- package/src/index.mts +7 -0
- package/src/plugin.mts +9 -0
- package/src/rule-types.mts +326 -0
- package/src/rules/ast-utils.mts +169 -0
- package/src/rules/branded-number-types.mts +47 -0
- package/src/rules/constants.mts +1 -0
- package/src/rules/import-utils.mts +152 -0
- package/src/rules/index.mts +1 -0
- package/src/rules/no-unnecessary-type-guard.mts +515 -0
- package/src/rules/no-unnecessary-type-guard.test.mts +368 -0
- package/src/rules/prefer-arr-is-array.mts +98 -0
- package/src/rules/prefer-arr-is-array.test.mts +62 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +143 -0
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +154 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +149 -0
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +214 -0
- package/src/rules/prefer-arr-is-max-length-array.mts +141 -0
- package/src/rules/prefer-arr-is-max-length-array.test.mts +188 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +141 -0
- package/src/rules/prefer-arr-is-min-length-array.test.mts +202 -0
- package/src/rules/prefer-arr-is-non-empty.mts +131 -0
- package/src/rules/prefer-arr-is-non-empty.test.mts +90 -0
- package/src/rules/prefer-arr-sum.mts +269 -0
- package/src/rules/prefer-arr-sum.test.mts +171 -0
- package/src/rules/prefer-as-int.mts +131 -0
- package/src/rules/prefer-as-int.test.mts +267 -0
- package/src/rules/prefer-canonical-array-slicing.mts +800 -0
- package/src/rules/prefer-canonical-array-slicing.test.mts +588 -0
- package/src/rules/prefer-comparison-over-nullish-guard.mts +141 -0
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +171 -0
- package/src/rules/prefer-is-non-null-object.mts +145 -0
- package/src/rules/prefer-is-non-null-object.test.mts +156 -0
- package/src/rules/prefer-is-record-and-has-key.mts +156 -0
- package/src/rules/prefer-is-record-and-has-key.test.mts +217 -0
- package/src/rules/prefer-num-safe-parse-float.mts +166 -0
- package/src/rules/prefer-num-safe-parse-float.test.mts +163 -0
- package/src/rules/prefer-num-safe-parse-int.mts +166 -0
- package/src/rules/prefer-num-safe-parse-int.test.mts +137 -0
- package/src/rules/prefer-range-for-loop.mts +179 -0
- package/src/rules/prefer-range-for-loop.test.mts +156 -0
- package/src/rules/rules.mts +36 -0
- package/src/types.mts +4 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import * as ts from 'typescript';
|
|
7
|
+
import {
|
|
8
|
+
buildImportFixes,
|
|
9
|
+
getNamedImports,
|
|
10
|
+
getTsDataForgeImport,
|
|
11
|
+
} from './import-utils.mjs';
|
|
12
|
+
|
|
13
|
+
type Options = readonly [];
|
|
14
|
+
|
|
15
|
+
type MessageIds = 'useSafeParseInt';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns `true` when every constituent of `type` is string-like, i.e. the type
|
|
19
|
+
* is assignable to the `string` parameter of `Num.safeParseInt`. A union such
|
|
20
|
+
* as `string | undefined` is rejected so the autofix never produces a type
|
|
21
|
+
* error.
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
24
|
+
const isStringType = (type: ts.Type): boolean => {
|
|
25
|
+
const parts = type.isUnion() ? type.types : ([type] as const);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
parts.length > 0 &&
|
|
29
|
+
parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detects the base-10 forms of `parseInt` / `Number.parseInt` (`parseInt(x)` or
|
|
35
|
+
* `parseInt(x, 10)`) and replaces them with
|
|
36
|
+
* `Result.unwrapOkOr(Num.safeParseInt(x), Number.NaN)` (ts-data-forge).
|
|
37
|
+
*
|
|
38
|
+
* Other radixes (e.g. `parseInt(x, 16)`) are intentionally left untouched, as
|
|
39
|
+
* `Num.safeParseInt` only supports base 10.
|
|
40
|
+
*/
|
|
41
|
+
export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
42
|
+
meta: {
|
|
43
|
+
type: 'suggestion',
|
|
44
|
+
docs: {
|
|
45
|
+
description:
|
|
46
|
+
'Replace `parseInt(x, 10)` with `Result.unwrapOkOr(Num.safeParseInt(x), Number.NaN)` from ts-data-forge.',
|
|
47
|
+
},
|
|
48
|
+
fixable: 'code',
|
|
49
|
+
schema: [],
|
|
50
|
+
messages: {
|
|
51
|
+
useSafeParseInt:
|
|
52
|
+
'Replace `{{original}}` with `Result.unwrapOkOr(Num.safeParseInt({{argName}}), Number.NaN)` from ts-data-forge.',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
create: (context) => {
|
|
57
|
+
const sourceCode = context.sourceCode;
|
|
58
|
+
|
|
59
|
+
const program = sourceCode.ast;
|
|
60
|
+
|
|
61
|
+
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
62
|
+
|
|
63
|
+
const services = context.sourceCode.parserServices;
|
|
64
|
+
|
|
65
|
+
const mut_nodesToFix: {
|
|
66
|
+
node: TSESTree.CallExpression;
|
|
67
|
+
argExpression: TSESTree.Expression;
|
|
68
|
+
}[] = [];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
CallExpression: (node) => {
|
|
72
|
+
const { callee } = node;
|
|
73
|
+
|
|
74
|
+
// Match `parseInt(...)` or `Number.parseInt(...)`.
|
|
75
|
+
const isGlobalParseInt =
|
|
76
|
+
callee.type === AST_NODE_TYPES.Identifier &&
|
|
77
|
+
callee.name === 'parseInt';
|
|
78
|
+
|
|
79
|
+
const isNumberParseInt =
|
|
80
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
81
|
+
!callee.computed &&
|
|
82
|
+
callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
83
|
+
callee.object.name === 'Number' &&
|
|
84
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
85
|
+
callee.property.name === 'parseInt';
|
|
86
|
+
|
|
87
|
+
if (!isGlobalParseInt && !isNumberParseInt) return;
|
|
88
|
+
|
|
89
|
+
const args = node.arguments;
|
|
90
|
+
|
|
91
|
+
const firstArg = args[0];
|
|
92
|
+
|
|
93
|
+
if (firstArg === undefined) return;
|
|
94
|
+
|
|
95
|
+
// Spread argument (`parseInt(...rest)`) cannot be rewritten safely.
|
|
96
|
+
if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;
|
|
97
|
+
|
|
98
|
+
// Only base 10: accept a missing radix or an explicit literal `10`.
|
|
99
|
+
if (args.length >= 2) {
|
|
100
|
+
const radix = args[1];
|
|
101
|
+
|
|
102
|
+
if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The argument must be a `string` so the autofix is type-safe against
|
|
108
|
+
// `Num.safeParseInt(s: string)`. Without type information, skip.
|
|
109
|
+
if (services?.program == null) return;
|
|
110
|
+
|
|
111
|
+
const checker = services.program.getTypeChecker();
|
|
112
|
+
|
|
113
|
+
const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
|
|
114
|
+
|
|
115
|
+
if (tsNode === undefined) return;
|
|
116
|
+
|
|
117
|
+
const argType = checker.getTypeAtLocation(tsNode);
|
|
118
|
+
|
|
119
|
+
if (!isStringType(argType)) return;
|
|
120
|
+
|
|
121
|
+
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
122
|
+
},
|
|
123
|
+
'Program:exit': () => {
|
|
124
|
+
const namedImports = getNamedImports(tsDataForgeImport);
|
|
125
|
+
|
|
126
|
+
const missingImports = (['Num', 'Result'] as const).filter(
|
|
127
|
+
(name) => !namedImports.includes(name),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
for (const [
|
|
131
|
+
index,
|
|
132
|
+
{ node, argExpression },
|
|
133
|
+
] of mut_nodesToFix.entries()) {
|
|
134
|
+
const argText = sourceCode.getText(argExpression);
|
|
135
|
+
|
|
136
|
+
const originalText = sourceCode.getText(node);
|
|
137
|
+
|
|
138
|
+
context.report({
|
|
139
|
+
node,
|
|
140
|
+
messageId: 'useSafeParseInt',
|
|
141
|
+
data: {
|
|
142
|
+
original: originalText,
|
|
143
|
+
argName: argText,
|
|
144
|
+
},
|
|
145
|
+
fix: (fixer) => {
|
|
146
|
+
const core = `Result.unwrapOkOr(Num.safeParseInt(${argText}), Number.NaN)`;
|
|
147
|
+
|
|
148
|
+
const importFixes =
|
|
149
|
+
index === 0 && missingImports.length > 0
|
|
150
|
+
? buildImportFixes(
|
|
151
|
+
fixer,
|
|
152
|
+
program,
|
|
153
|
+
tsDataForgeImport,
|
|
154
|
+
missingImports,
|
|
155
|
+
)
|
|
156
|
+
: [];
|
|
157
|
+
|
|
158
|
+
return [...importFixes, fixer.replaceText(node, core)];
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
defaultOptions: [],
|
|
166
|
+
} as const;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferNumSafeParseInt } from './prefer-num-safe-parse-int.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
projectService: {
|
|
13
|
+
allowDefaultProject: ['*.ts*'],
|
|
14
|
+
},
|
|
15
|
+
tsconfigRootDir: `${import.meta.dirname}/../..`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('prefer-num-safe-parse-int', () => {
|
|
21
|
+
tester.run('prefer-num-safe-parse-int', preferNumSafeParseInt, {
|
|
22
|
+
valid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'ignores non-base-10 radix',
|
|
25
|
+
code: dedent`
|
|
26
|
+
const s = "ff";
|
|
27
|
+
const n = parseInt(s, 16);
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'ignores number arguments (autofix would be unsafe)',
|
|
32
|
+
code: dedent`
|
|
33
|
+
const x = 42;
|
|
34
|
+
const n = parseInt(x, 10);
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'ignores never-typed arguments',
|
|
39
|
+
code: dedent`
|
|
40
|
+
declare const x: string & number;
|
|
41
|
+
const n = parseInt(x, 10);
|
|
42
|
+
`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'ignores unrelated calls',
|
|
46
|
+
code: dedent`
|
|
47
|
+
const s = "1";
|
|
48
|
+
const n = Number(s);
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
invalid: [
|
|
53
|
+
{
|
|
54
|
+
name: 'replaces parseInt(s, 10) and imports Num + Result',
|
|
55
|
+
code: dedent`
|
|
56
|
+
const s = "123";
|
|
57
|
+
const n = parseInt(s, 10);
|
|
58
|
+
`,
|
|
59
|
+
output: dedent`
|
|
60
|
+
import { Num, Result } from 'ts-data-forge';
|
|
61
|
+
const s = "123";
|
|
62
|
+
const n = Result.unwrapOkOr(Num.safeParseInt(s), Number.NaN);
|
|
63
|
+
`,
|
|
64
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'replaces parseInt(s) without radix',
|
|
68
|
+
code: dedent`
|
|
69
|
+
const s = "123";
|
|
70
|
+
const n = parseInt(s);
|
|
71
|
+
`,
|
|
72
|
+
output: dedent`
|
|
73
|
+
import { Num, Result } from 'ts-data-forge';
|
|
74
|
+
const s = "123";
|
|
75
|
+
const n = Result.unwrapOkOr(Num.safeParseInt(s), Number.NaN);
|
|
76
|
+
`,
|
|
77
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'replaces Number.parseInt(s, 10)',
|
|
81
|
+
code: dedent`
|
|
82
|
+
const s = "123";
|
|
83
|
+
const n = Number.parseInt(s, 10);
|
|
84
|
+
`,
|
|
85
|
+
output: dedent`
|
|
86
|
+
import { Num, Result } from 'ts-data-forge';
|
|
87
|
+
const s = "123";
|
|
88
|
+
const n = Result.unwrapOkOr(Num.safeParseInt(s), Number.NaN);
|
|
89
|
+
`,
|
|
90
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'adds only the missing Result import when Num is present',
|
|
94
|
+
code: dedent`
|
|
95
|
+
import { Num } from 'ts-data-forge';
|
|
96
|
+
|
|
97
|
+
const s = "123";
|
|
98
|
+
const n = parseInt(s, 10);
|
|
99
|
+
`,
|
|
100
|
+
output: dedent`
|
|
101
|
+
import { Result } from 'ts-data-forge';
|
|
102
|
+
import { Num } from 'ts-data-forge';
|
|
103
|
+
|
|
104
|
+
const s = "123";
|
|
105
|
+
const n = Result.unwrapOkOr(Num.safeParseInt(s), Number.NaN);
|
|
106
|
+
`,
|
|
107
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'wraps in parentheses in an operator context',
|
|
111
|
+
code: dedent`
|
|
112
|
+
const s = "123";
|
|
113
|
+
const ok = parseInt(s, 10) > 5;
|
|
114
|
+
`,
|
|
115
|
+
output: dedent`
|
|
116
|
+
import { Num, Result } from 'ts-data-forge';
|
|
117
|
+
const s = "123";
|
|
118
|
+
const ok = Result.unwrapOkOr(Num.safeParseInt(s), Number.NaN) > 5;
|
|
119
|
+
`,
|
|
120
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'preserves a complex string argument expression',
|
|
124
|
+
code: dedent`
|
|
125
|
+
const get = (k: string): string => k;
|
|
126
|
+
const n = parseInt(get("x").trim(), 10);
|
|
127
|
+
`,
|
|
128
|
+
output: dedent`
|
|
129
|
+
import { Num, Result } from 'ts-data-forge';
|
|
130
|
+
const get = (k: string): string => k;
|
|
131
|
+
const n = Result.unwrapOkOr(Num.safeParseInt(get("x").trim()), Number.NaN);
|
|
132
|
+
`,
|
|
133
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
}, 20000);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import { Arr } from 'ts-data-forge';
|
|
7
|
+
import {
|
|
8
|
+
buildImportFixes,
|
|
9
|
+
getNamedImports,
|
|
10
|
+
getTsDataForgeImport,
|
|
11
|
+
} from './import-utils.mjs';
|
|
12
|
+
|
|
13
|
+
type Options = readonly [];
|
|
14
|
+
|
|
15
|
+
type MessageIds = 'useRangeForLoop';
|
|
16
|
+
|
|
17
|
+
export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'suggestion',
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
'Replace `for (let i = begin; i < end; ++i)` with `for (const i of range(begin, end))` from ts-data-forge.',
|
|
23
|
+
},
|
|
24
|
+
fixable: 'code',
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
useRangeForLoop:
|
|
28
|
+
'Replace with `for (const {{varName}} of range({{begin}}, {{end}}))` from ts-data-forge.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create: (context) => {
|
|
33
|
+
const sourceCode = context.sourceCode;
|
|
34
|
+
|
|
35
|
+
const program = sourceCode.ast;
|
|
36
|
+
|
|
37
|
+
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
38
|
+
|
|
39
|
+
const mut_nodesToFix: {
|
|
40
|
+
node: TSESTree.ForStatement;
|
|
41
|
+
varName: string;
|
|
42
|
+
begin: string;
|
|
43
|
+
end: string;
|
|
44
|
+
step?: string;
|
|
45
|
+
}[] = [];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
ForStatement: (node) => {
|
|
49
|
+
// Check init: let i = begin
|
|
50
|
+
if (
|
|
51
|
+
node.init?.type !== AST_NODE_TYPES.VariableDeclaration ||
|
|
52
|
+
node.init.kind !== 'let' ||
|
|
53
|
+
!Arr.isFixedLengthTuple(node.init.declarations, 1)
|
|
54
|
+
) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const declaration = node.init.declarations[0];
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
declaration.id.type !== AST_NODE_TYPES.Identifier ||
|
|
62
|
+
declaration.init === null
|
|
63
|
+
) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const varName = declaration.id.name;
|
|
68
|
+
|
|
69
|
+
const beginExpr = declaration.init;
|
|
70
|
+
|
|
71
|
+
// Check test: i < end
|
|
72
|
+
if (
|
|
73
|
+
node.test?.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
74
|
+
node.test.operator !== '<' ||
|
|
75
|
+
node.test.left.type !== AST_NODE_TYPES.Identifier ||
|
|
76
|
+
node.test.left.name !== varName
|
|
77
|
+
) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const endExpr = node.test.right;
|
|
82
|
+
|
|
83
|
+
// Check update: ++i, i++, i += 1, or i += step
|
|
84
|
+
if (node.update === null) return;
|
|
85
|
+
|
|
86
|
+
let mut_step: string | undefined;
|
|
87
|
+
|
|
88
|
+
if (node.update.type === AST_NODE_TYPES.UpdateExpression) {
|
|
89
|
+
// ++i or i++
|
|
90
|
+
if (
|
|
91
|
+
node.update.operator === '++' &&
|
|
92
|
+
node.update.argument.type === AST_NODE_TYPES.Identifier &&
|
|
93
|
+
node.update.argument.name === varName
|
|
94
|
+
) {
|
|
95
|
+
mut_step = undefined; // step defaults to 1
|
|
96
|
+
} else {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
} else if (
|
|
100
|
+
node.update.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
101
|
+
node.update.operator === '+=' &&
|
|
102
|
+
node.update.left.type === AST_NODE_TYPES.Identifier &&
|
|
103
|
+
node.update.left.name === varName
|
|
104
|
+
) {
|
|
105
|
+
// i += step
|
|
106
|
+
const stepValue = node.update.right;
|
|
107
|
+
|
|
108
|
+
if (stepValue.type === AST_NODE_TYPES.Literal) {
|
|
109
|
+
const stepNum = stepValue.value;
|
|
110
|
+
|
|
111
|
+
// Only support positive integer steps
|
|
112
|
+
if (typeof stepNum === 'number' && stepNum > 0) {
|
|
113
|
+
mut_step = stepNum === 1 ? undefined : String(stepNum);
|
|
114
|
+
} else {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Variable step like i += step
|
|
119
|
+
mut_step = sourceCode.getText(stepValue);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const beginText = sourceCode.getText(beginExpr);
|
|
126
|
+
|
|
127
|
+
const endText = sourceCode.getText(endExpr);
|
|
128
|
+
|
|
129
|
+
mut_nodesToFix.push({
|
|
130
|
+
node,
|
|
131
|
+
varName,
|
|
132
|
+
begin: beginText,
|
|
133
|
+
end: endText,
|
|
134
|
+
step: mut_step,
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
'Program:exit': () => {
|
|
138
|
+
const namedImports = getNamedImports(tsDataForgeImport);
|
|
139
|
+
|
|
140
|
+
const hasRangeImport = namedImports.includes('range');
|
|
141
|
+
|
|
142
|
+
for (const [
|
|
143
|
+
index,
|
|
144
|
+
{ node, varName, begin, end, step },
|
|
145
|
+
] of mut_nodesToFix.entries()) {
|
|
146
|
+
context.report({
|
|
147
|
+
node: node.init ?? node,
|
|
148
|
+
messageId: 'useRangeForLoop',
|
|
149
|
+
data: {
|
|
150
|
+
varName,
|
|
151
|
+
begin,
|
|
152
|
+
end,
|
|
153
|
+
},
|
|
154
|
+
fix: (fixer) => {
|
|
155
|
+
const bodyText = sourceCode.getText(node.body);
|
|
156
|
+
|
|
157
|
+
const rangeArgs =
|
|
158
|
+
step === undefined
|
|
159
|
+
? `${begin}, ${end}`
|
|
160
|
+
: `${begin}, ${end}, ${step}`;
|
|
161
|
+
|
|
162
|
+
const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;
|
|
163
|
+
|
|
164
|
+
const importFixes =
|
|
165
|
+
index === 0 && !hasRangeImport
|
|
166
|
+
? buildImportFixes(fixer, program, tsDataForgeImport, [
|
|
167
|
+
'range',
|
|
168
|
+
])
|
|
169
|
+
: [];
|
|
170
|
+
|
|
171
|
+
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
defaultOptions: [],
|
|
179
|
+
} as const;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferRangeForLoop } from './prefer-range-for-loop.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
projectService: {
|
|
13
|
+
allowDefaultProject: ['*.ts*'],
|
|
14
|
+
},
|
|
15
|
+
tsconfigRootDir: `${import.meta.dirname}/../..`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('prefer-range-for-loop', () => {
|
|
21
|
+
tester.run('prefer-range-for-loop', preferRangeForLoop, {
|
|
22
|
+
valid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'ignores for loop with const',
|
|
25
|
+
code: dedent`
|
|
26
|
+
for (const i = 0; i < 10; ++i) {
|
|
27
|
+
console.log(i);
|
|
28
|
+
}
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'ignores for loop with different condition',
|
|
33
|
+
code: dedent`
|
|
34
|
+
for (let i = 0; i <= 10; ++i) {
|
|
35
|
+
console.log(i);
|
|
36
|
+
}
|
|
37
|
+
`,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
invalid: [
|
|
41
|
+
{
|
|
42
|
+
name: 'replaces for loop with ++i',
|
|
43
|
+
code: dedent`
|
|
44
|
+
for (let i = 0; i < 10; ++i) {
|
|
45
|
+
console.log(i);
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
output: dedent`
|
|
49
|
+
import { range } from 'ts-data-forge';
|
|
50
|
+
for (const i of range(0, 10)) {
|
|
51
|
+
console.log(i);
|
|
52
|
+
}
|
|
53
|
+
`,
|
|
54
|
+
errors: [{ messageId: 'useRangeForLoop' }],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'replaces for loop with i++',
|
|
58
|
+
code: dedent`
|
|
59
|
+
for (let i = 0; i < 10; i++) {
|
|
60
|
+
console.log(i);
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
output: dedent`
|
|
64
|
+
import { range } from 'ts-data-forge';
|
|
65
|
+
for (const i of range(0, 10)) {
|
|
66
|
+
console.log(i);
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
errors: [{ messageId: 'useRangeForLoop' }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'replaces for loop with i += 1',
|
|
73
|
+
code: dedent`
|
|
74
|
+
for (let i = 0; i < 10; i += 1) {
|
|
75
|
+
console.log(i);
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
output: dedent`
|
|
79
|
+
import { range } from 'ts-data-forge';
|
|
80
|
+
for (const i of range(0, 10)) {
|
|
81
|
+
console.log(i);
|
|
82
|
+
}
|
|
83
|
+
`,
|
|
84
|
+
errors: [{ messageId: 'useRangeForLoop' }],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'replaces for loop with variable bounds',
|
|
88
|
+
code: dedent`
|
|
89
|
+
const begin = 5;
|
|
90
|
+
const end = 15;
|
|
91
|
+
for (let j = begin; j < end; ++j) {
|
|
92
|
+
console.log(j);
|
|
93
|
+
}
|
|
94
|
+
`,
|
|
95
|
+
output: dedent`
|
|
96
|
+
import { range } from 'ts-data-forge';
|
|
97
|
+
const begin = 5;
|
|
98
|
+
const end = 15;
|
|
99
|
+
for (const j of range(begin, end)) {
|
|
100
|
+
console.log(j);
|
|
101
|
+
}
|
|
102
|
+
`,
|
|
103
|
+
errors: [{ messageId: 'useRangeForLoop' }],
|
|
104
|
+
},
|
|
105
|
+
// {
|
|
106
|
+
// name: 'replaces for loop with different condition',
|
|
107
|
+
// code: dedent`
|
|
108
|
+
// for (let i = 0; i <= 10; ++i) {
|
|
109
|
+
// console.log(i);
|
|
110
|
+
// }
|
|
111
|
+
// `,
|
|
112
|
+
// output: dedent`
|
|
113
|
+
// import { range } from 'ts-data-forge';
|
|
114
|
+
// for (const i of range(0, 11)) {
|
|
115
|
+
// console.log(i);
|
|
116
|
+
// }
|
|
117
|
+
// `,
|
|
118
|
+
// errors: [{ messageId: 'useRangeForLoop' }],
|
|
119
|
+
// },
|
|
120
|
+
{
|
|
121
|
+
name: 'replaces for loop with i += 2',
|
|
122
|
+
code: dedent`
|
|
123
|
+
for (let i = 0; i < 10; i += 2) {
|
|
124
|
+
console.log(i);
|
|
125
|
+
}
|
|
126
|
+
`,
|
|
127
|
+
output: dedent`
|
|
128
|
+
import { range } from 'ts-data-forge';
|
|
129
|
+
for (const i of range(0, 10, 2)) {
|
|
130
|
+
console.log(i);
|
|
131
|
+
}
|
|
132
|
+
`,
|
|
133
|
+
errors: [{ messageId: 'useRangeForLoop' }],
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
name: 'keeps existing range import',
|
|
138
|
+
code: dedent`
|
|
139
|
+
import { range } from 'ts-data-forge';
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < 10; ++i) {
|
|
142
|
+
console.log(i);
|
|
143
|
+
}
|
|
144
|
+
`,
|
|
145
|
+
output: dedent`
|
|
146
|
+
import { range } from 'ts-data-forge';
|
|
147
|
+
|
|
148
|
+
for (const i of range(0, 10)) {
|
|
149
|
+
console.log(i);
|
|
150
|
+
}
|
|
151
|
+
`,
|
|
152
|
+
errors: [{ messageId: 'useRangeForLoop' }],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
}, 20000);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ESLintPlugin } from '../types.mjs';
|
|
2
|
+
import { noUnnecessaryTypeGuard } from './no-unnecessary-type-guard.mjs';
|
|
3
|
+
import { preferArrIsArray } from './prefer-arr-is-array.mjs';
|
|
4
|
+
import { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.mjs';
|
|
5
|
+
import { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.mjs';
|
|
6
|
+
import { preferArrIsMaxLengthArray } from './prefer-arr-is-max-length-array.mjs';
|
|
7
|
+
import { preferArrIsMinLengthArray } from './prefer-arr-is-min-length-array.mjs';
|
|
8
|
+
import { preferArrIsNonEmpty } from './prefer-arr-is-non-empty.mjs';
|
|
9
|
+
import { preferArrSum } from './prefer-arr-sum.mjs';
|
|
10
|
+
import { preferAsInt } from './prefer-as-int.mjs';
|
|
11
|
+
import { preferCanonicalArraySlicing } from './prefer-canonical-array-slicing.mjs';
|
|
12
|
+
import { preferComparisonOverNullishGuard } from './prefer-comparison-over-nullish-guard.mjs';
|
|
13
|
+
import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
|
|
14
|
+
import { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';
|
|
15
|
+
import { preferNumSafeParseFloat } from './prefer-num-safe-parse-float.mjs';
|
|
16
|
+
import { preferNumSafeParseInt } from './prefer-num-safe-parse-int.mjs';
|
|
17
|
+
import { preferRangeForLoop } from './prefer-range-for-loop.mjs';
|
|
18
|
+
|
|
19
|
+
export const tsDataForgeRules = {
|
|
20
|
+
'prefer-canonical-array-slicing': preferCanonicalArraySlicing,
|
|
21
|
+
'prefer-arr-is-min-length-array': preferArrIsMinLengthArray,
|
|
22
|
+
'prefer-arr-is-max-length-array': preferArrIsMaxLengthArray,
|
|
23
|
+
'prefer-arr-is-bounded-length-array': preferArrIsBoundedLengthArray,
|
|
24
|
+
'prefer-arr-is-fixed-length-array': preferArrIsFixedLengthArray,
|
|
25
|
+
'prefer-arr-is-array': preferArrIsArray,
|
|
26
|
+
'prefer-arr-is-non-empty': preferArrIsNonEmpty,
|
|
27
|
+
'prefer-arr-sum': preferArrSum,
|
|
28
|
+
'prefer-as-int': preferAsInt,
|
|
29
|
+
'prefer-is-non-null-object': preferIsNonNullObject,
|
|
30
|
+
'prefer-range-for-loop': preferRangeForLoop,
|
|
31
|
+
'prefer-is-record-and-has-key': preferIsRecordAndHasKey,
|
|
32
|
+
'prefer-num-safe-parse-int': preferNumSafeParseInt,
|
|
33
|
+
'prefer-num-safe-parse-float': preferNumSafeParseFloat,
|
|
34
|
+
'no-unnecessary-type-guard': noUnnecessaryTypeGuard,
|
|
35
|
+
'prefer-comparison-over-nullish-guard': preferComparisonOverNullishGuard,
|
|
36
|
+
} as const satisfies ESLintPlugin['rules'];
|
package/src/types.mts
ADDED