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,154 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.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-arr-is-bounded-length-array', () => {
|
|
21
|
+
tester.run(
|
|
22
|
+
'prefer-arr-is-bounded-length-array',
|
|
23
|
+
preferArrIsBoundedLengthArray,
|
|
24
|
+
{
|
|
25
|
+
valid: [
|
|
26
|
+
{
|
|
27
|
+
name: 'ignores non-array types',
|
|
28
|
+
code: dedent`
|
|
29
|
+
const str = "hello";
|
|
30
|
+
const ok = str.length >= 1 && str.length <= 5;
|
|
31
|
+
`,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'ignores a single min bound',
|
|
35
|
+
code: dedent`
|
|
36
|
+
const xs = [1, 2, 3];
|
|
37
|
+
const ok = xs.length >= 1;
|
|
38
|
+
`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'ignores a single max bound',
|
|
42
|
+
code: dedent`
|
|
43
|
+
const xs = [1, 2, 3];
|
|
44
|
+
const ok = xs.length <= 5;
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'ignores two bounds of the same kind',
|
|
49
|
+
code: dedent`
|
|
50
|
+
const xs = [1, 2, 3];
|
|
51
|
+
const ok = xs.length >= 1 && xs.length >= 2;
|
|
52
|
+
`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'ignores bounds on different arrays',
|
|
56
|
+
code: dedent`
|
|
57
|
+
const xs = [1, 2, 3];
|
|
58
|
+
const ys = [4, 5];
|
|
59
|
+
const ok = xs.length >= 1 && ys.length <= 5;
|
|
60
|
+
`,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'ignores || combinations',
|
|
64
|
+
code: dedent`
|
|
65
|
+
const xs = [1, 2, 3];
|
|
66
|
+
const ok = xs.length >= 1 || xs.length <= 5;
|
|
67
|
+
`,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'ignores comparison with non-const variable',
|
|
71
|
+
code: dedent`
|
|
72
|
+
const xs = [1, 2, 3];
|
|
73
|
+
let lo = 1;
|
|
74
|
+
const ok = xs.length >= lo && xs.length <= 5;
|
|
75
|
+
`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
invalid: [
|
|
79
|
+
{
|
|
80
|
+
name: 'replaces min && max with Arr.isBoundedLengthArray',
|
|
81
|
+
code: dedent`
|
|
82
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
83
|
+
const ok = xs.length >= 1 && xs.length <= 5;
|
|
84
|
+
`,
|
|
85
|
+
output: dedent`
|
|
86
|
+
import { Arr } from 'ts-data-forge';
|
|
87
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
88
|
+
const ok = Arr.isBoundedLengthArray(xs, 1, 5);
|
|
89
|
+
`,
|
|
90
|
+
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'normalizes max && min ordering to (min, max)',
|
|
94
|
+
code: dedent`
|
|
95
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
96
|
+
const ok = xs.length <= 5 && xs.length >= 1;
|
|
97
|
+
`,
|
|
98
|
+
output: dedent`
|
|
99
|
+
import { Arr } from 'ts-data-forge';
|
|
100
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
101
|
+
const ok = Arr.isBoundedLengthArray(xs, 1, 5);
|
|
102
|
+
`,
|
|
103
|
+
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'handles reversed operand order in each comparison',
|
|
107
|
+
code: dedent`
|
|
108
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
109
|
+
const ok = 1 <= xs.length && 5 >= xs.length;
|
|
110
|
+
`,
|
|
111
|
+
output: dedent`
|
|
112
|
+
import { Arr } from 'ts-data-forge';
|
|
113
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
114
|
+
const ok = Arr.isBoundedLengthArray(xs, 1, 5);
|
|
115
|
+
`,
|
|
116
|
+
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'works with const variable bounds',
|
|
120
|
+
code: dedent`
|
|
121
|
+
const xs = [1, 2, 3];
|
|
122
|
+
const lo = 1;
|
|
123
|
+
const hi = 5;
|
|
124
|
+
const ok = xs.length >= lo && xs.length <= hi;
|
|
125
|
+
`,
|
|
126
|
+
output: dedent`
|
|
127
|
+
import { Arr } from 'ts-data-forge';
|
|
128
|
+
const xs = [1, 2, 3];
|
|
129
|
+
const lo = 1;
|
|
130
|
+
const hi = 5;
|
|
131
|
+
const ok = Arr.isBoundedLengthArray(xs, lo, hi);
|
|
132
|
+
`,
|
|
133
|
+
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'keeps existing Arr import',
|
|
137
|
+
code: dedent`
|
|
138
|
+
import { Arr } from 'ts-data-forge';
|
|
139
|
+
|
|
140
|
+
const xs = [1, 2, 3];
|
|
141
|
+
const ok = xs.length >= 1 && xs.length <= 5;
|
|
142
|
+
`,
|
|
143
|
+
output: dedent`
|
|
144
|
+
import { Arr } from 'ts-data-forge';
|
|
145
|
+
|
|
146
|
+
const xs = [1, 2, 3];
|
|
147
|
+
const ok = Arr.isBoundedLengthArray(xs, 1, 5);
|
|
148
|
+
`,
|
|
149
|
+
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
}, 20000);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import { type DeepReadonly } from 'ts-type-forge';
|
|
7
|
+
import { isIntegerLiteralOrConstant } from './ast-utils.mjs';
|
|
8
|
+
import {
|
|
9
|
+
buildImportFixes,
|
|
10
|
+
getNamedImports,
|
|
11
|
+
getTsDataForgeImport,
|
|
12
|
+
} from './import-utils.mjs';
|
|
13
|
+
|
|
14
|
+
type Options = readonly [];
|
|
15
|
+
|
|
16
|
+
type MessageIds = 'useIsFixedLengthArray';
|
|
17
|
+
|
|
18
|
+
export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
19
|
+
MessageIds,
|
|
20
|
+
Options
|
|
21
|
+
> = {
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'suggestion',
|
|
24
|
+
docs: {
|
|
25
|
+
description:
|
|
26
|
+
'Replace `xs.length === n` with `Arr.isFixedLengthArray(xs, n)` from ts-data-forge.',
|
|
27
|
+
},
|
|
28
|
+
fixable: 'code',
|
|
29
|
+
schema: [],
|
|
30
|
+
messages: {
|
|
31
|
+
useIsFixedLengthArray:
|
|
32
|
+
'Replace `{{original}}` with `Arr.isFixedLengthArray({{arrayName}}, {{length}})` from ts-data-forge.',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
create: (context) => {
|
|
37
|
+
const sourceCode = context.sourceCode;
|
|
38
|
+
|
|
39
|
+
const program = sourceCode.ast;
|
|
40
|
+
|
|
41
|
+
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
42
|
+
|
|
43
|
+
const services = context.sourceCode.parserServices;
|
|
44
|
+
|
|
45
|
+
const mut_nodesToFix: {
|
|
46
|
+
node: TSESTree.BinaryExpression;
|
|
47
|
+
arrayExpression: TSESTree.Expression;
|
|
48
|
+
lengthExpression: TSESTree.Expression;
|
|
49
|
+
isNegated: boolean;
|
|
50
|
+
}[] = [];
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
BinaryExpression: (node) => {
|
|
54
|
+
// Check for `xs.length === n` or `n === xs.length` or `xs.length !== n`
|
|
55
|
+
if (node.operator !== '===' && node.operator !== '!==') return;
|
|
56
|
+
|
|
57
|
+
const isNegated = node.operator === '!==';
|
|
58
|
+
|
|
59
|
+
const isLengthOnLeft = isLengthAccess(node.left);
|
|
60
|
+
|
|
61
|
+
const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
|
|
62
|
+
|
|
63
|
+
const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
|
|
64
|
+
|
|
65
|
+
if (!isLengthAccess(lengthSide)) return;
|
|
66
|
+
|
|
67
|
+
// Only match integer literals or const variables initialized with integer literals
|
|
68
|
+
if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) return;
|
|
69
|
+
|
|
70
|
+
// lengthSide is MemberExpression accessing .length
|
|
71
|
+
const arrayExpression = lengthSide.object;
|
|
72
|
+
|
|
73
|
+
// Check if arrayExpression is actually an array type
|
|
74
|
+
if (services?.program !== undefined && services.program !== null) {
|
|
75
|
+
const checker = services.program.getTypeChecker();
|
|
76
|
+
|
|
77
|
+
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
78
|
+
|
|
79
|
+
if (tsNode !== undefined) {
|
|
80
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
81
|
+
|
|
82
|
+
// Check if it's an array type or tuple type
|
|
83
|
+
const isArrayType =
|
|
84
|
+
checker.isArrayType(type) || checker.isTupleType(type);
|
|
85
|
+
|
|
86
|
+
if (!isArrayType) return;
|
|
87
|
+
} else {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
mut_nodesToFix.push({
|
|
95
|
+
node,
|
|
96
|
+
arrayExpression,
|
|
97
|
+
lengthExpression: valueSide,
|
|
98
|
+
isNegated,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
'Program:exit': () => {
|
|
102
|
+
const namedImports = getNamedImports(tsDataForgeImport);
|
|
103
|
+
|
|
104
|
+
const hasArrImport = namedImports.includes('Arr');
|
|
105
|
+
|
|
106
|
+
for (const [
|
|
107
|
+
index,
|
|
108
|
+
{ node, arrayExpression, lengthExpression, isNegated },
|
|
109
|
+
] of mut_nodesToFix.entries()) {
|
|
110
|
+
const arrayText = sourceCode.getText(arrayExpression);
|
|
111
|
+
|
|
112
|
+
const lengthText = sourceCode.getText(lengthExpression);
|
|
113
|
+
|
|
114
|
+
const originalText = sourceCode.getText(node);
|
|
115
|
+
|
|
116
|
+
context.report({
|
|
117
|
+
node,
|
|
118
|
+
messageId: 'useIsFixedLengthArray',
|
|
119
|
+
data: {
|
|
120
|
+
original: originalText,
|
|
121
|
+
arrayName: arrayText,
|
|
122
|
+
length: lengthText,
|
|
123
|
+
},
|
|
124
|
+
fix: (fixer) => {
|
|
125
|
+
const baseCall = `Arr.isFixedLengthArray(${arrayText}, ${lengthText})`;
|
|
126
|
+
|
|
127
|
+
const replacement = isNegated ? `!${baseCall}` : baseCall;
|
|
128
|
+
|
|
129
|
+
const importFixes =
|
|
130
|
+
index === 0 && !hasArrImport
|
|
131
|
+
? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
|
|
132
|
+
: [];
|
|
133
|
+
|
|
134
|
+
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
defaultOptions: [],
|
|
142
|
+
} as const;
|
|
143
|
+
|
|
144
|
+
const isLengthAccess = (
|
|
145
|
+
node: DeepReadonly<TSESTree.Expression>,
|
|
146
|
+
): node is TSESTree.MemberExpression =>
|
|
147
|
+
node.type === AST_NODE_TYPES.MemberExpression &&
|
|
148
|
+
node.property.type === AST_NODE_TYPES.Identifier &&
|
|
149
|
+
node.property.name === 'length';
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.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-arr-is-fixed-length-array', () => {
|
|
21
|
+
tester.run('prefer-arr-is-fixed-length-array', preferArrIsFixedLengthArray, {
|
|
22
|
+
valid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'ignores non-array types',
|
|
25
|
+
code: dedent`
|
|
26
|
+
const str = "hello";
|
|
27
|
+
const ok = str.length === 5;
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'ignores other comparisons',
|
|
32
|
+
code: dedent`
|
|
33
|
+
const xs = [1, 2, 3];
|
|
34
|
+
const ok = xs.length > 0;
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'ignores comparison with non-const variable',
|
|
39
|
+
code: dedent`
|
|
40
|
+
const xs = [1, 2, 3];
|
|
41
|
+
let n = 3;
|
|
42
|
+
const ok = xs.length === n;
|
|
43
|
+
`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'ignores comparison with function return value',
|
|
47
|
+
code: dedent`
|
|
48
|
+
const xs = [1, 2, 3];
|
|
49
|
+
const ok = xs.length === Math.floor(3.5);
|
|
50
|
+
`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'ignores comparison with const variable initialized by non-literal',
|
|
54
|
+
code: dedent`
|
|
55
|
+
const xs = [1, 2, 3];
|
|
56
|
+
const n = Math.floor(3.5);
|
|
57
|
+
const ok = xs.length === n;
|
|
58
|
+
`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'ignores comparison with const variable of type number',
|
|
62
|
+
code: dedent`
|
|
63
|
+
const xs = [1, 2, 3];
|
|
64
|
+
const n: number = 3;
|
|
65
|
+
const ok = xs.length === n;
|
|
66
|
+
`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
invalid: [
|
|
70
|
+
{
|
|
71
|
+
name: 'replaces xs.length === n with Arr.isFixedLengthArray',
|
|
72
|
+
code: dedent`
|
|
73
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
74
|
+
const ok = xs.length === 3;
|
|
75
|
+
`,
|
|
76
|
+
output: dedent`
|
|
77
|
+
import { Arr } from 'ts-data-forge';
|
|
78
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
79
|
+
const ok = Arr.isFixedLengthArray(xs, 3);
|
|
80
|
+
`,
|
|
81
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'replaces n === xs.length with Arr.isFixedLengthArray',
|
|
85
|
+
code: dedent`
|
|
86
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
87
|
+
const ok = 3 === xs.length;
|
|
88
|
+
`,
|
|
89
|
+
output: dedent`
|
|
90
|
+
import { Arr } from 'ts-data-forge';
|
|
91
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
92
|
+
const ok = Arr.isFixedLengthArray(xs, 3);
|
|
93
|
+
`,
|
|
94
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'works with no type annotation',
|
|
98
|
+
code: dedent`
|
|
99
|
+
const xs = [1, 2, 3];
|
|
100
|
+
const ok = xs.length === 3;
|
|
101
|
+
`,
|
|
102
|
+
output: dedent`
|
|
103
|
+
import { Arr } from 'ts-data-forge';
|
|
104
|
+
const xs = [1, 2, 3];
|
|
105
|
+
const ok = Arr.isFixedLengthArray(xs, 3);
|
|
106
|
+
`,
|
|
107
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'works with const assertion',
|
|
111
|
+
code: dedent`
|
|
112
|
+
const xs = [1, 2, 3] as const;
|
|
113
|
+
const ok = xs.length === 3;
|
|
114
|
+
`,
|
|
115
|
+
output: dedent`
|
|
116
|
+
import { Arr } from 'ts-data-forge';
|
|
117
|
+
const xs = [1, 2, 3] as const;
|
|
118
|
+
const ok = Arr.isFixedLengthArray(xs, 3);
|
|
119
|
+
`,
|
|
120
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'works with variable length',
|
|
124
|
+
code: dedent`
|
|
125
|
+
const xs = [1, 2, 3];
|
|
126
|
+
const n = 3;
|
|
127
|
+
const ok = xs.length === n;
|
|
128
|
+
`,
|
|
129
|
+
output: dedent`
|
|
130
|
+
import { Arr } from 'ts-data-forge';
|
|
131
|
+
const xs = [1, 2, 3];
|
|
132
|
+
const n = 3;
|
|
133
|
+
const ok = Arr.isFixedLengthArray(xs, n);
|
|
134
|
+
`,
|
|
135
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'keeps existing Arr import',
|
|
139
|
+
code: dedent`
|
|
140
|
+
import { Arr } from 'ts-data-forge';
|
|
141
|
+
|
|
142
|
+
const xs = [1, 2, 3];
|
|
143
|
+
const ok = xs.length === 3;
|
|
144
|
+
`,
|
|
145
|
+
output: dedent`
|
|
146
|
+
import { Arr } from 'ts-data-forge';
|
|
147
|
+
|
|
148
|
+
const xs = [1, 2, 3];
|
|
149
|
+
const ok = Arr.isFixedLengthArray(xs, 3);
|
|
150
|
+
`,
|
|
151
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'replaces multiple checks',
|
|
155
|
+
code: dedent`
|
|
156
|
+
const xs = [1, 2, 3];
|
|
157
|
+
const ys = [4, 5];
|
|
158
|
+
const ok1 = xs.length === 3;
|
|
159
|
+
const ok2 = ys.length === 2;
|
|
160
|
+
`,
|
|
161
|
+
output: dedent`
|
|
162
|
+
import { Arr } from 'ts-data-forge';
|
|
163
|
+
const xs = [1, 2, 3];
|
|
164
|
+
const ys = [4, 5];
|
|
165
|
+
const ok1 = Arr.isFixedLengthArray(xs, 3);
|
|
166
|
+
const ok2 = Arr.isFixedLengthArray(ys, 2);
|
|
167
|
+
`,
|
|
168
|
+
errors: [
|
|
169
|
+
{ messageId: 'useIsFixedLengthArray' },
|
|
170
|
+
{ messageId: 'useIsFixedLengthArray' },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'replaces xs.length !== n with !Arr.isFixedLengthArray',
|
|
175
|
+
code: dedent`
|
|
176
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
177
|
+
const ok = xs.length !== 3;
|
|
178
|
+
`,
|
|
179
|
+
output: dedent`
|
|
180
|
+
import { Arr } from 'ts-data-forge';
|
|
181
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
182
|
+
const ok = !Arr.isFixedLengthArray(xs, 3);
|
|
183
|
+
`,
|
|
184
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'replaces n !== xs.length with !Arr.isFixedLengthArray',
|
|
188
|
+
code: dedent`
|
|
189
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
190
|
+
const ok = 3 !== xs.length;
|
|
191
|
+
`,
|
|
192
|
+
output: dedent`
|
|
193
|
+
import { Arr } from 'ts-data-forge';
|
|
194
|
+
const xs: readonly number[] = [1, 2, 3];
|
|
195
|
+
const ok = !Arr.isFixedLengthArray(xs, 3);
|
|
196
|
+
`,
|
|
197
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'works with !== and no type annotation',
|
|
201
|
+
code: dedent`
|
|
202
|
+
const xs = [1, 2, 3];
|
|
203
|
+
const ok = xs.length !== 5;
|
|
204
|
+
`,
|
|
205
|
+
output: dedent`
|
|
206
|
+
import { Arr } from 'ts-data-forge';
|
|
207
|
+
const xs = [1, 2, 3];
|
|
208
|
+
const ok = !Arr.isFixedLengthArray(xs, 5);
|
|
209
|
+
`,
|
|
210
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
}, 20000);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import {
|
|
3
|
+
isIntegerLiteralOrConstant,
|
|
4
|
+
isLengthAccess,
|
|
5
|
+
isPartOfBoundedLengthCheck,
|
|
6
|
+
} from './ast-utils.mjs';
|
|
7
|
+
import {
|
|
8
|
+
buildImportFixes,
|
|
9
|
+
getNamedImports,
|
|
10
|
+
getTsDataForgeImport,
|
|
11
|
+
} from './import-utils.mjs';
|
|
12
|
+
|
|
13
|
+
type Options = readonly [];
|
|
14
|
+
|
|
15
|
+
type MessageIds = 'useIsMaxLengthArray';
|
|
16
|
+
|
|
17
|
+
export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
|
|
18
|
+
MessageIds,
|
|
19
|
+
Options
|
|
20
|
+
> = {
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'suggestion',
|
|
23
|
+
docs: {
|
|
24
|
+
description:
|
|
25
|
+
'Replace `xs.length <= n` with `Arr.isMaxLengthArray(xs, n)` from ts-data-forge.',
|
|
26
|
+
},
|
|
27
|
+
fixable: 'code',
|
|
28
|
+
schema: [],
|
|
29
|
+
messages: {
|
|
30
|
+
useIsMaxLengthArray:
|
|
31
|
+
'Replace `{{original}}` with `Arr.isMaxLengthArray({{arrayName}}, {{length}})` from ts-data-forge.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
create: (context) => {
|
|
36
|
+
const sourceCode = context.sourceCode;
|
|
37
|
+
|
|
38
|
+
const program = sourceCode.ast;
|
|
39
|
+
|
|
40
|
+
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
41
|
+
|
|
42
|
+
const services = context.sourceCode.parserServices;
|
|
43
|
+
|
|
44
|
+
const mut_nodesToFix: {
|
|
45
|
+
node: TSESTree.BinaryExpression;
|
|
46
|
+
arrayExpression: TSESTree.Expression;
|
|
47
|
+
lengthExpression: TSESTree.Expression;
|
|
48
|
+
}[] = [];
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
BinaryExpression: (node) => {
|
|
52
|
+
// Check for `xs.length <= n` or `n >= xs.length`
|
|
53
|
+
if (node.operator !== '>=' && node.operator !== '<=') return;
|
|
54
|
+
|
|
55
|
+
// Defer to `prefer-arr-is-bounded-length-array` when this comparison is
|
|
56
|
+
// the upper bound of a `min && max` pair on the same array.
|
|
57
|
+
if (isPartOfBoundedLengthCheck(node, sourceCode)) return;
|
|
58
|
+
|
|
59
|
+
// xs.length <= n or n >= xs.length
|
|
60
|
+
const isLengthOnLeft = node.operator === '<=';
|
|
61
|
+
|
|
62
|
+
const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
|
|
63
|
+
|
|
64
|
+
const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
|
|
65
|
+
|
|
66
|
+
// Check if lengthSide is accessing .length
|
|
67
|
+
if (!isLengthAccess(lengthSide)) return;
|
|
68
|
+
|
|
69
|
+
// Only match integer literals or const variables initialized with integer literals
|
|
70
|
+
if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) return;
|
|
71
|
+
|
|
72
|
+
// lengthSide is MemberExpression accessing .length
|
|
73
|
+
const arrayExpression = lengthSide.object;
|
|
74
|
+
|
|
75
|
+
// Check if arrayExpression is actually an array type
|
|
76
|
+
if (services?.program !== undefined && services.program !== null) {
|
|
77
|
+
const checker = services.program.getTypeChecker();
|
|
78
|
+
|
|
79
|
+
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
80
|
+
|
|
81
|
+
if (tsNode !== undefined) {
|
|
82
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
83
|
+
|
|
84
|
+
// Check if it's an array type or tuple type
|
|
85
|
+
const isArrayType =
|
|
86
|
+
checker.isArrayType(type) || checker.isTupleType(type);
|
|
87
|
+
|
|
88
|
+
if (!isArrayType) return;
|
|
89
|
+
} else {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mut_nodesToFix.push({
|
|
97
|
+
node,
|
|
98
|
+
arrayExpression,
|
|
99
|
+
lengthExpression: valueSide,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
'Program:exit': () => {
|
|
103
|
+
const namedImports = getNamedImports(tsDataForgeImport);
|
|
104
|
+
|
|
105
|
+
const hasArrImport = namedImports.includes('Arr');
|
|
106
|
+
|
|
107
|
+
for (const [
|
|
108
|
+
index,
|
|
109
|
+
{ node, arrayExpression, lengthExpression },
|
|
110
|
+
] of mut_nodesToFix.entries()) {
|
|
111
|
+
const arrayText = sourceCode.getText(arrayExpression);
|
|
112
|
+
|
|
113
|
+
const lengthText = sourceCode.getText(lengthExpression);
|
|
114
|
+
|
|
115
|
+
const originalText = sourceCode.getText(node);
|
|
116
|
+
|
|
117
|
+
context.report({
|
|
118
|
+
node,
|
|
119
|
+
messageId: 'useIsMaxLengthArray',
|
|
120
|
+
data: {
|
|
121
|
+
original: originalText,
|
|
122
|
+
arrayName: arrayText,
|
|
123
|
+
length: lengthText,
|
|
124
|
+
},
|
|
125
|
+
fix: (fixer) => {
|
|
126
|
+
const replacement = `Arr.isMaxLengthArray(${arrayText}, ${lengthText})`;
|
|
127
|
+
|
|
128
|
+
const importFixes =
|
|
129
|
+
index === 0 && !hasArrImport
|
|
130
|
+
? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
|
|
131
|
+
: [];
|
|
132
|
+
|
|
133
|
+
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
defaultOptions: [],
|
|
141
|
+
} as const;
|