eslint-plugin-ts-data-forge 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +2 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +15 -0
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.d.mts +6 -0
- package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -0
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs +218 -0
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -0
- package/dist/rules/rules.d.mts +1 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +3 -1
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +10 -10
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +16 -0
- package/src/rules/prefer-obj-over-entries-round-trip.mts +546 -0
- package/src/rules/prefer-obj-over-entries-round-trip.test.mts +397 -0
- package/src/rules/rules.mts +2 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferObjOverEntriesRoundTrip } from './prefer-obj-over-entries-round-trip.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-obj-over-entries-round-trip', () => {
|
|
21
|
+
tester.run(
|
|
22
|
+
'prefer-obj-over-entries-round-trip',
|
|
23
|
+
preferObjOverEntriesRoundTrip,
|
|
24
|
+
{
|
|
25
|
+
valid: [
|
|
26
|
+
{
|
|
27
|
+
name: 'ignores an entries round trip that rewrites the key',
|
|
28
|
+
code: dedent`
|
|
29
|
+
const scores = { alice: 1, bob: 2 };
|
|
30
|
+
const upper = Object.fromEntries(
|
|
31
|
+
Object.entries(scores).map(([k, v]) => [k.toUpperCase(), v]),
|
|
32
|
+
);
|
|
33
|
+
`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'ignores a callback with a block body',
|
|
37
|
+
code: dedent`
|
|
38
|
+
const scores = { alice: 1, bob: 2 };
|
|
39
|
+
const doubled = Object.fromEntries(
|
|
40
|
+
Object.entries(scores).map(([k, v]) => {
|
|
41
|
+
return [k, v * 2];
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'ignores a callback that takes the entry index',
|
|
48
|
+
code: dedent`
|
|
49
|
+
const scores = { alice: 1, bob: 2 };
|
|
50
|
+
const indexed = Object.fromEntries(
|
|
51
|
+
Object.entries(scores).map(([k, v], i) => [k, v + i]),
|
|
52
|
+
);
|
|
53
|
+
`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'ignores a callback that does not destructure the entry',
|
|
57
|
+
code: dedent`
|
|
58
|
+
const scores = { alice: 1, bob: 2 };
|
|
59
|
+
const same = Object.fromEntries(
|
|
60
|
+
Object.entries(scores).map((entry) => entry),
|
|
61
|
+
);
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'ignores a callback passed by reference',
|
|
66
|
+
code: dedent`
|
|
67
|
+
const scores: Record<string, number> = { alice: 1, bob: 2 };
|
|
68
|
+
const isPositive = ([, v]: readonly [string, number]) => v > 0;
|
|
69
|
+
const positive = Object.fromEntries(
|
|
70
|
+
Object.entries(scores).filter(isPositive),
|
|
71
|
+
);
|
|
72
|
+
`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'ignores an entry pattern with a default or a rest element',
|
|
76
|
+
code: dedent`
|
|
77
|
+
const scores = { alice: 1, bob: 2 };
|
|
78
|
+
const a = Object.fromEntries(
|
|
79
|
+
Object.entries(scores).map(([k, v = 0]) => [k, v * 2]),
|
|
80
|
+
);
|
|
81
|
+
const b = Object.fromEntries(
|
|
82
|
+
Object.entries(scores).map(([k, ...rest]) => [k, rest.length]),
|
|
83
|
+
);
|
|
84
|
+
`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'ignores an entry tuple asserted to a type of its own',
|
|
88
|
+
code: dedent`
|
|
89
|
+
const scores = { alice: 1, bob: 2 };
|
|
90
|
+
const widened = Object.fromEntries(
|
|
91
|
+
Object.entries(scores).map(
|
|
92
|
+
([k, v]) => [k, v] as [string, number | undefined],
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
`,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'ignores a callback with an explicit return type',
|
|
99
|
+
code: dedent`
|
|
100
|
+
const scores = { alice: 1, bob: 2 };
|
|
101
|
+
const passing = Object.fromEntries(
|
|
102
|
+
Object.entries(scores).filter(([, v]): boolean => v > 1),
|
|
103
|
+
);
|
|
104
|
+
`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'ignores an async callback',
|
|
108
|
+
code: dedent`
|
|
109
|
+
const scores = { alice: 1, bob: 2 };
|
|
110
|
+
const mapped = Object.fromEntries(
|
|
111
|
+
Object.entries(scores).map(async ([k, v]) => [k, v * 2]),
|
|
112
|
+
);
|
|
113
|
+
`,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'ignores a flatMap that is not a keep/drop conditional',
|
|
117
|
+
code: dedent`
|
|
118
|
+
const scores = { alice: 1, bob: 2 };
|
|
119
|
+
const a = Object.fromEntries(
|
|
120
|
+
Object.entries(scores).flatMap(([k, v]) => [[k, v * 2]]),
|
|
121
|
+
);
|
|
122
|
+
const b = Object.fromEntries(
|
|
123
|
+
Object.entries(scores).flatMap(([k, v]) =>
|
|
124
|
+
v > 1 ? [[k, v]] : [[k, 0]],
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
`,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'ignores a round trip through something other than Object.entries',
|
|
131
|
+
code: dedent`
|
|
132
|
+
const scores = { alice: 1, bob: 2 };
|
|
133
|
+
const a = Object.fromEntries(
|
|
134
|
+
Object.keys(scores).map((k) => [k, 0]),
|
|
135
|
+
);
|
|
136
|
+
const entries: (readonly [string, number])[] = [['a', 1]];
|
|
137
|
+
const b = Object.fromEntries(entries.map(([k, v]) => [k, v * 2]));
|
|
138
|
+
`,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'ignores Object.entries without the Object.fromEntries wrapper',
|
|
142
|
+
code: dedent`
|
|
143
|
+
const scores = { alice: 1, bob: 2 };
|
|
144
|
+
const pairs = Object.entries(scores).map(([k, v]) => [k, v * 2]);
|
|
145
|
+
`,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'ignores an array method Obj has no counterpart for',
|
|
149
|
+
code: dedent`
|
|
150
|
+
const scores = { alice: 1, bob: 2 };
|
|
151
|
+
const sorted = Object.fromEntries(
|
|
152
|
+
Object.entries(scores).toSorted(([a], [b]) => a.localeCompare(b)),
|
|
153
|
+
);
|
|
154
|
+
`,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
|
|
158
|
+
invalid: [
|
|
159
|
+
{
|
|
160
|
+
name: 'replaces a value-only map with Obj.map',
|
|
161
|
+
code: dedent`
|
|
162
|
+
const scores = { alice: 1, bob: 2 };
|
|
163
|
+
const doubled = Object.fromEntries(
|
|
164
|
+
Object.entries(scores).map(([k, v]) => [k, v * 2]),
|
|
165
|
+
);
|
|
166
|
+
`,
|
|
167
|
+
output: dedent`
|
|
168
|
+
import { Obj } from 'ts-data-forge';
|
|
169
|
+
const scores = { alice: 1, bob: 2 };
|
|
170
|
+
const doubled = Obj.map(scores, (v) => v * 2);
|
|
171
|
+
`,
|
|
172
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'keeps the key parameter when the mapped value uses it',
|
|
176
|
+
code: dedent`
|
|
177
|
+
const scores = { alice: 1, bob: 2 };
|
|
178
|
+
const labelled = Object.fromEntries(
|
|
179
|
+
Object.entries(scores).map(([k, v]) => [k, k.length + v]),
|
|
180
|
+
);
|
|
181
|
+
`,
|
|
182
|
+
output: dedent`
|
|
183
|
+
import { Obj } from 'ts-data-forge';
|
|
184
|
+
const scores = { alice: 1, bob: 2 };
|
|
185
|
+
const labelled = Obj.map(scores, (v, k) => k.length + v);
|
|
186
|
+
`,
|
|
187
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'drops both parameters when the mapped value uses neither',
|
|
191
|
+
code: dedent`
|
|
192
|
+
const scores = { alice: 1, bob: 2 };
|
|
193
|
+
const zeroed = Object.fromEntries(
|
|
194
|
+
Object.entries(scores).map(([k]) => [k, 0]),
|
|
195
|
+
);
|
|
196
|
+
`,
|
|
197
|
+
output: dedent`
|
|
198
|
+
import { Obj } from 'ts-data-forge';
|
|
199
|
+
const scores = { alice: 1, bob: 2 };
|
|
200
|
+
const zeroed = Obj.map(scores, () => 0);
|
|
201
|
+
`,
|
|
202
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'looks through the as const an entry tuple carries',
|
|
206
|
+
code: dedent`
|
|
207
|
+
const scores = { alice: 1, bob: 2 };
|
|
208
|
+
const doubled = Object.fromEntries(
|
|
209
|
+
Object.entries(scores).map(([k, v]) => [k, v * 2] as const),
|
|
210
|
+
);
|
|
211
|
+
`,
|
|
212
|
+
output: dedent`
|
|
213
|
+
import { Obj } from 'ts-data-forge';
|
|
214
|
+
const scores = { alice: 1, bob: 2 };
|
|
215
|
+
const doubled = Obj.map(scores, (v) => v * 2);
|
|
216
|
+
`,
|
|
217
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'parenthesizes an object-literal body',
|
|
221
|
+
code: dedent`
|
|
222
|
+
const scores = { alice: 1, bob: 2 };
|
|
223
|
+
const wrapped = Object.fromEntries(
|
|
224
|
+
Object.entries(scores).map(([k, v]) => [k, { value: v }]),
|
|
225
|
+
);
|
|
226
|
+
`,
|
|
227
|
+
output: dedent`
|
|
228
|
+
import { Obj } from 'ts-data-forge';
|
|
229
|
+
const scores = { alice: 1, bob: 2 };
|
|
230
|
+
const wrapped = Obj.map(scores, (v) => ({ value: v }));
|
|
231
|
+
`,
|
|
232
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'parenthesizes an object literal that keeps its own as const',
|
|
236
|
+
code: dedent`
|
|
237
|
+
const scores = { alice: 1, bob: 2 };
|
|
238
|
+
const wrapped = Object.fromEntries(
|
|
239
|
+
Object.entries(scores).map(([k, v]) => [k, { value: v } as const]),
|
|
240
|
+
);
|
|
241
|
+
`,
|
|
242
|
+
output: dedent`
|
|
243
|
+
import { Obj } from 'ts-data-forge';
|
|
244
|
+
const scores = { alice: 1, bob: 2 };
|
|
245
|
+
const wrapped = Obj.map(scores, (v) => ({ value: v } as const));
|
|
246
|
+
`,
|
|
247
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'replaces a value predicate with Obj.filter',
|
|
251
|
+
code: dedent`
|
|
252
|
+
const scores = { alice: 1, bob: 2 };
|
|
253
|
+
const passing = Object.fromEntries(
|
|
254
|
+
Object.entries(scores).filter(([, v]) => v > 1),
|
|
255
|
+
);
|
|
256
|
+
`,
|
|
257
|
+
output: dedent`
|
|
258
|
+
import { Obj } from 'ts-data-forge';
|
|
259
|
+
const scores = { alice: 1, bob: 2 };
|
|
260
|
+
const passing = Obj.filter(scores, (v) => v > 1);
|
|
261
|
+
`,
|
|
262
|
+
errors: [{ messageId: 'useObjFilter' }],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'names an unused value parameter when the predicate only uses the key',
|
|
266
|
+
code: dedent`
|
|
267
|
+
const scores = { alice: 1, bob: 2 };
|
|
268
|
+
const notAlice = Object.fromEntries(
|
|
269
|
+
Object.entries(scores).filter(([k]) => k !== 'alice'),
|
|
270
|
+
);
|
|
271
|
+
`,
|
|
272
|
+
output: dedent`
|
|
273
|
+
import { Obj } from 'ts-data-forge';
|
|
274
|
+
const scores = { alice: 1, bob: 2 };
|
|
275
|
+
const notAlice = Obj.filter(scores, (_value, k) => k !== 'alice');
|
|
276
|
+
`,
|
|
277
|
+
errors: [{ messageId: 'useObjFilter' }],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'swaps the entry bindings into value-then-key order',
|
|
281
|
+
code: dedent`
|
|
282
|
+
const scores = { alice: 1, bob: 2 };
|
|
283
|
+
const kept = Object.fromEntries(
|
|
284
|
+
Object.entries(scores).filter(([k, v]) => k !== 'alice' && v > 0),
|
|
285
|
+
);
|
|
286
|
+
`,
|
|
287
|
+
output: dedent`
|
|
288
|
+
import { Obj } from 'ts-data-forge';
|
|
289
|
+
const scores = { alice: 1, bob: 2 };
|
|
290
|
+
const kept = Obj.filter(scores, (v, k) => k !== 'alice' && v > 0);
|
|
291
|
+
`,
|
|
292
|
+
errors: [{ messageId: 'useObjFilter' }],
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: 'replaces a keep/drop flatMap with Obj.filterMap',
|
|
296
|
+
code: dedent`
|
|
297
|
+
const scores = { alice: 1, bob: 2 };
|
|
298
|
+
const scaled = Object.fromEntries(
|
|
299
|
+
Object.entries(scores).flatMap(([k, v]) =>
|
|
300
|
+
v > 1 ? [[k, v * 10]] : [],
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
`,
|
|
304
|
+
output: dedent`
|
|
305
|
+
import { Obj, Optional } from 'ts-data-forge';
|
|
306
|
+
const scores = { alice: 1, bob: 2 };
|
|
307
|
+
const scaled = Obj.filterMap(scores, (v) => v > 1 ? Optional.some(v * 10) : Optional.none);
|
|
308
|
+
`,
|
|
309
|
+
errors: [{ messageId: 'useObjFilterMap' }],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'handles a flatMap whose branches are the other way round',
|
|
313
|
+
code: dedent`
|
|
314
|
+
const scores = { alice: 1, bob: 2 };
|
|
315
|
+
const scaled = Object.fromEntries(
|
|
316
|
+
Object.entries(scores).flatMap(([k, v]) =>
|
|
317
|
+
v > 1 ? [] : [[k, v * 10] as const],
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
`,
|
|
321
|
+
output: dedent`
|
|
322
|
+
import { Obj, Optional } from 'ts-data-forge';
|
|
323
|
+
const scores = { alice: 1, bob: 2 };
|
|
324
|
+
const scaled = Obj.filterMap(scores, (v) => v > 1 ? Optional.none : Optional.some(v * 10));
|
|
325
|
+
`,
|
|
326
|
+
errors: [{ messageId: 'useObjFilterMap' }],
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: 'reuses an existing ts-data-forge import',
|
|
330
|
+
code: dedent`
|
|
331
|
+
import { Obj } from 'ts-data-forge';
|
|
332
|
+
const scores = { alice: 1, bob: 2 };
|
|
333
|
+
const doubled = Object.fromEntries(
|
|
334
|
+
Object.entries(scores).map(([k, v]) => [k, v * 2]),
|
|
335
|
+
);
|
|
336
|
+
`,
|
|
337
|
+
output: dedent`
|
|
338
|
+
import { Obj } from 'ts-data-forge';
|
|
339
|
+
const scores = { alice: 1, bob: 2 };
|
|
340
|
+
const doubled = Obj.map(scores, (v) => v * 2);
|
|
341
|
+
`,
|
|
342
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'uses the local name of an aliased import',
|
|
346
|
+
code: dedent`
|
|
347
|
+
import { Obj as O } from 'ts-data-forge';
|
|
348
|
+
const scores = { alice: 1, bob: 2 };
|
|
349
|
+
const doubled = Object.fromEntries(
|
|
350
|
+
Object.entries(scores).map(([k, v]) => [k, v * 2]),
|
|
351
|
+
);
|
|
352
|
+
`,
|
|
353
|
+
output: dedent`
|
|
354
|
+
import { Obj as O } from 'ts-data-forge';
|
|
355
|
+
const scores = { alice: 1, bob: 2 };
|
|
356
|
+
const doubled = O.map(scores, (v) => v * 2);
|
|
357
|
+
`,
|
|
358
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'imports Obj once for several round trips in one file',
|
|
362
|
+
code: dedent`
|
|
363
|
+
const scores = { alice: 1, bob: 2 };
|
|
364
|
+
const doubled = Object.fromEntries(
|
|
365
|
+
Object.entries(scores).map(([k, v]) => [k, v * 2]),
|
|
366
|
+
);
|
|
367
|
+
const passing = Object.fromEntries(
|
|
368
|
+
Object.entries(scores).filter(([, v]) => v > 1),
|
|
369
|
+
);
|
|
370
|
+
`,
|
|
371
|
+
output: dedent`
|
|
372
|
+
import { Obj } from 'ts-data-forge';
|
|
373
|
+
const scores = { alice: 1, bob: 2 };
|
|
374
|
+
const doubled = Obj.map(scores, (v) => v * 2);
|
|
375
|
+
const passing = Obj.filter(scores, (v) => v > 1);
|
|
376
|
+
`,
|
|
377
|
+
errors: [{ messageId: 'useObjMap' }, { messageId: 'useObjFilter' }],
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: 'rewrites a round trip over a computed record expression',
|
|
381
|
+
code: dedent`
|
|
382
|
+
const wrap = (r: Record<string, number>) => r;
|
|
383
|
+
const doubled = Object.fromEntries(
|
|
384
|
+
Object.entries(wrap({ a: 1 })).map(([k, v]) => [k, v * 2]),
|
|
385
|
+
);
|
|
386
|
+
`,
|
|
387
|
+
output: dedent`
|
|
388
|
+
import { Obj } from 'ts-data-forge';
|
|
389
|
+
const wrap = (r: Record<string, number>) => r;
|
|
390
|
+
const doubled = Obj.map(wrap({ a: 1 }), (v) => v * 2);
|
|
391
|
+
`,
|
|
392
|
+
errors: [{ messageId: 'useObjMap' }],
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
});
|
package/src/rules/rules.mts
CHANGED
|
@@ -11,6 +11,7 @@ import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
|
|
|
11
11
|
import { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';
|
|
12
12
|
import { preferNumSafeParseFloat } from './prefer-num-safe-parse-float.mjs';
|
|
13
13
|
import { preferNumSafeParseInt } from './prefer-num-safe-parse-int.mjs';
|
|
14
|
+
import { preferObjOverEntriesRoundTrip } from './prefer-obj-over-entries-round-trip.mjs';
|
|
14
15
|
import { preferRangeForLoop } from './prefer-range-for-loop.mjs';
|
|
15
16
|
|
|
16
17
|
export const tsDataForgeRules = {
|
|
@@ -27,4 +28,5 @@ export const tsDataForgeRules = {
|
|
|
27
28
|
'prefer-num-safe-parse-float': preferNumSafeParseFloat,
|
|
28
29
|
'no-unnecessary-type-guard': noUnnecessaryTypeGuard,
|
|
29
30
|
'prefer-comparison-over-nullish-guard': preferComparisonOverNullishGuard,
|
|
31
|
+
'prefer-obj-over-entries-round-trip': preferObjOverEntriesRoundTrip,
|
|
30
32
|
} as const satisfies ESLintPlugin['rules'];
|