@mrpalmer/eslint-plugin 1.0.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/lib/constants.d.ts +2 -0
- package/lib/constants.js +1 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +31 -0
- package/lib/meta.d.ts +4 -0
- package/lib/meta.js +17 -0
- package/lib/rules/shorten-paths.d.ts +8 -0
- package/lib/rules/shorten-paths.js +70 -0
- package/lib/rules/sort-imports.d.ts +19 -0
- package/lib/rules/sort-imports.js +636 -0
- package/lib/rules/sort-named.d.ts +8 -0
- package/lib/rules/sort-named.js +436 -0
- package/lib/types.d.ts +12 -0
- package/lib/types.js +1 -0
- package/lib/utils/array.d.ts +6 -0
- package/lib/utils/array.js +23 -0
- package/lib/utils/ast.d.ts +4 -0
- package/lib/utils/ast.js +102 -0
- package/lib/utils/create-rule.d.ts +2 -0
- package/lib/utils/create-rule.js +58 -0
- package/lib/utils/import-path-options.d.ts +6 -0
- package/lib/utils/import-path-options.js +98 -0
- package/lib/utils/import-type.d.ts +3 -0
- package/lib/utils/import-type.js +88 -0
- package/lib/utils/settings.d.ts +3 -0
- package/lib/utils/settings.js +20 -0
- package/lib/utils/shortest-path.d.ts +2 -0
- package/lib/utils/shortest-path.js +45 -0
- package/lib/utils/ts-config.d.ts +16 -0
- package/lib/utils/ts-config.js +54 -0
- package/package.json +31 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { groupBy, reverse, findOutOfOrder } from '../utils/array.js';
|
|
3
|
+
import { createRule } from '../utils/create-rule.js';
|
|
4
|
+
//region rule
|
|
5
|
+
// REPORTING AND FIXING
|
|
6
|
+
function findSpecifierStart(sourceCode, node) {
|
|
7
|
+
let token;
|
|
8
|
+
do {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
10
|
+
token = sourceCode.getTokenBefore(node);
|
|
11
|
+
} while (token.value !== ',' && token.value !== '{');
|
|
12
|
+
return token.range[1];
|
|
13
|
+
}
|
|
14
|
+
function findSpecifierEnd(sourceCode, node) {
|
|
15
|
+
let token;
|
|
16
|
+
do {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
18
|
+
token = sourceCode.getTokenAfter(node);
|
|
19
|
+
} while (token.value !== ',' && token.value !== '}');
|
|
20
|
+
return token.range[0];
|
|
21
|
+
}
|
|
22
|
+
function isRequireExpression(expr) {
|
|
23
|
+
return (expr != null &&
|
|
24
|
+
expr.type === TSESTree.AST_NODE_TYPES.CallExpression &&
|
|
25
|
+
expr.callee.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
26
|
+
expr.callee.name === 'require' &&
|
|
27
|
+
expr.arguments.length === 1 &&
|
|
28
|
+
expr.arguments[0].type === TSESTree.AST_NODE_TYPES.Literal);
|
|
29
|
+
}
|
|
30
|
+
function isCJSExports(context, node) {
|
|
31
|
+
if (node.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
32
|
+
node.object.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
33
|
+
node.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
34
|
+
node.object.name === 'module' &&
|
|
35
|
+
node.property.name === 'exports') {
|
|
36
|
+
return !context.sourceCode
|
|
37
|
+
.getScope(node)
|
|
38
|
+
.variables.some((variable) => variable.name === 'module');
|
|
39
|
+
}
|
|
40
|
+
if (node.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
41
|
+
node.name === 'exports') {
|
|
42
|
+
return !context.sourceCode
|
|
43
|
+
.getScope(node)
|
|
44
|
+
.variables.some((variable) => variable.name === 'exports');
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
function getNamedCJSExports(context, node) {
|
|
49
|
+
if (node.type !== TSESTree.AST_NODE_TYPES.MemberExpression) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const result = [];
|
|
53
|
+
let root = node;
|
|
54
|
+
let parent;
|
|
55
|
+
while (root.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
|
|
56
|
+
if (root.property.type !== TSESTree.AST_NODE_TYPES.Identifier) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
result.unshift(root.property.name);
|
|
60
|
+
parent = root;
|
|
61
|
+
root = root.object;
|
|
62
|
+
}
|
|
63
|
+
if (isCJSExports(context, root)) {
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
if (isCJSExports(context, parent)) {
|
|
67
|
+
return result.slice(1);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function makeImportDescription(node) {
|
|
72
|
+
if (node.type === 'export') {
|
|
73
|
+
if (node.node.exportKind === 'type') {
|
|
74
|
+
return 'type export';
|
|
75
|
+
}
|
|
76
|
+
return 'export';
|
|
77
|
+
}
|
|
78
|
+
if (node.node.importKind === 'type') {
|
|
79
|
+
return 'type import';
|
|
80
|
+
}
|
|
81
|
+
return 'import';
|
|
82
|
+
}
|
|
83
|
+
function fixOutOfOrder(context, firstNode, secondNode, order) {
|
|
84
|
+
const { sourceCode } = context;
|
|
85
|
+
const { firstRoot, secondRoot } = {
|
|
86
|
+
firstRoot: firstNode.node,
|
|
87
|
+
secondRoot: secondNode.node,
|
|
88
|
+
};
|
|
89
|
+
const { firstRootStart, firstRootEnd, secondRootStart, secondRootEnd } = {
|
|
90
|
+
firstRootStart: findSpecifierStart(sourceCode, firstRoot),
|
|
91
|
+
firstRootEnd: findSpecifierEnd(sourceCode, firstRoot),
|
|
92
|
+
secondRootStart: findSpecifierStart(sourceCode, secondRoot),
|
|
93
|
+
secondRootEnd: findSpecifierEnd(sourceCode, secondRoot),
|
|
94
|
+
};
|
|
95
|
+
if (firstNode.displayName === secondNode.displayName) {
|
|
96
|
+
if (firstNode.alias) {
|
|
97
|
+
firstNode.displayName = `${firstNode.displayName} as ${firstNode.alias}`;
|
|
98
|
+
}
|
|
99
|
+
if (secondNode.alias) {
|
|
100
|
+
secondNode.displayName = `${secondNode.displayName} as ${secondNode.alias}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const firstDesc = makeImportDescription(firstNode);
|
|
104
|
+
const secondDesc = makeImportDescription(secondNode);
|
|
105
|
+
if (firstNode.displayName === secondNode.displayName &&
|
|
106
|
+
firstDesc === secondDesc) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const firstImport = `${firstDesc} of \`${firstNode.displayName}\``;
|
|
110
|
+
const secondImport = `\`${secondNode.displayName}\` ${secondDesc}`;
|
|
111
|
+
const messageOptions = {
|
|
112
|
+
messageId: 'order',
|
|
113
|
+
data: { firstImport, secondImport, order },
|
|
114
|
+
};
|
|
115
|
+
const firstCode = sourceCode.text.slice(firstRootStart, firstRoot.range[1]);
|
|
116
|
+
const firstTrivia = sourceCode.text.slice(firstRoot.range[1], firstRootEnd);
|
|
117
|
+
const secondCode = sourceCode.text.slice(secondRootStart, secondRoot.range[1]);
|
|
118
|
+
const secondTrivia = sourceCode.text.slice(secondRoot.range[1], secondRootEnd);
|
|
119
|
+
if (order === 'before') {
|
|
120
|
+
const trimmedTrivia = secondTrivia.trimEnd();
|
|
121
|
+
const gapCode = sourceCode.text.slice(firstRootEnd, secondRootStart - 1);
|
|
122
|
+
const whitespaces = secondTrivia.slice(trimmedTrivia.length);
|
|
123
|
+
context.report({
|
|
124
|
+
node: secondNode.node,
|
|
125
|
+
...messageOptions,
|
|
126
|
+
fix: (fixer) => fixer.replaceTextRange([firstRootStart, secondRootEnd], `${secondCode},${trimmedTrivia}${firstCode}${firstTrivia}${gapCode}${whitespaces}`),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const trimmedTrivia = firstTrivia.trimEnd();
|
|
131
|
+
const gapCode = sourceCode.text.slice(secondRootEnd + 1, firstRootStart);
|
|
132
|
+
const whitespaces = firstTrivia.slice(trimmedTrivia.length);
|
|
133
|
+
context.report({
|
|
134
|
+
node: secondNode.node,
|
|
135
|
+
...messageOptions,
|
|
136
|
+
fix: (fixes) => fixes.replaceTextRange([secondRootStart, firstRootEnd], `${gapCode}${firstCode},${trimmedTrivia}${secondCode}${whitespaces}`),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function reportOutOfOrder(context, imported, outOfOrder, order) {
|
|
141
|
+
for (const imp of outOfOrder) {
|
|
142
|
+
fixOutOfOrder(context,
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
144
|
+
imported.find((importedItem) => importedItem.rank > imp.rank), imp, order);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function compareStrings(a, b) {
|
|
148
|
+
const aChunks = a
|
|
149
|
+
.replace(/(\d+)/g, '\0$1\0')
|
|
150
|
+
.replace(/^\0/, '')
|
|
151
|
+
.replace(/\0$/, '')
|
|
152
|
+
.split('\0');
|
|
153
|
+
const bChunks = b
|
|
154
|
+
.replace(/(\d+)/g, '\0$1\0')
|
|
155
|
+
.replace(/^\0/, '')
|
|
156
|
+
.replace(/\0$/, '')
|
|
157
|
+
.split('\0');
|
|
158
|
+
const maxLength = Math.max(aChunks.length, bChunks.length);
|
|
159
|
+
for (let i = 0; i < maxLength; i++) {
|
|
160
|
+
const aChunk = aChunks[i] ?? '';
|
|
161
|
+
const bChunk = bChunks[i] ?? '';
|
|
162
|
+
const aNum = Number(aChunk);
|
|
163
|
+
const bNum = Number(bChunk);
|
|
164
|
+
if (isNaN(aNum)) {
|
|
165
|
+
if (!isNaN(bNum)) {
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (isNaN(bNum)) {
|
|
170
|
+
return -1;
|
|
171
|
+
}
|
|
172
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
173
|
+
if (aNum !== bNum) {
|
|
174
|
+
return aNum - bNum;
|
|
175
|
+
}
|
|
176
|
+
if (aChunk !== bChunk) {
|
|
177
|
+
if (aChunk < bChunk) {
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
if (aChunk > bChunk) {
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (aChunk < bChunk) {
|
|
187
|
+
return -1;
|
|
188
|
+
}
|
|
189
|
+
if (aChunk > bChunk) {
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
const getNormalizedValue = (nodeValue, toLowerCase) => {
|
|
196
|
+
const value = String(nodeValue);
|
|
197
|
+
return toLowerCase ? value.toLowerCase() : value;
|
|
198
|
+
};
|
|
199
|
+
function getSorter(ignoreCase) {
|
|
200
|
+
function compareValues(valueA, valueB) {
|
|
201
|
+
const normalizedA = getNormalizedValue(valueA, ignoreCase);
|
|
202
|
+
const normalizedB = getNormalizedValue(valueB, ignoreCase);
|
|
203
|
+
return compareStrings(normalizedA, normalizedB);
|
|
204
|
+
}
|
|
205
|
+
return function importsSorter(nodeA, nodeB) {
|
|
206
|
+
if (nodeA.value === nodeB.value) {
|
|
207
|
+
if (nodeA.alias && nodeB.alias) {
|
|
208
|
+
return compareValues(nodeA.alias, nodeB.alias);
|
|
209
|
+
}
|
|
210
|
+
if (nodeA.alias) {
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
if (nodeB.alias) {
|
|
214
|
+
return -1;
|
|
215
|
+
}
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
return compareValues(nodeA.value, nodeB.value);
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function mutateRanksToAlphabetize(imported, ignoreCase) {
|
|
222
|
+
const groupedByRanks = groupBy(imported, (item) => item.rank);
|
|
223
|
+
const sorterFn = getSorter(ignoreCase);
|
|
224
|
+
// sort group keys so that they can be iterated on in order
|
|
225
|
+
const groupRanks = Object.keys(groupedByRanks).sort((a, b) => +a - +b);
|
|
226
|
+
// sort imports locally within their group
|
|
227
|
+
for (const groupRank of groupRanks) {
|
|
228
|
+
groupedByRanks[groupRank].sort(sorterFn);
|
|
229
|
+
}
|
|
230
|
+
// assign globally unique rank to each import
|
|
231
|
+
let newRank = 0;
|
|
232
|
+
const alphabetizedRanks = groupRanks.reduce((acc, groupRank) => {
|
|
233
|
+
for (const importedItem of groupedByRanks[groupRank]) {
|
|
234
|
+
acc[`${importedItem.key}|${importedItem.node.importKind}`] =
|
|
235
|
+
Number.parseInt(groupRank, 10) + newRank;
|
|
236
|
+
newRank += 1;
|
|
237
|
+
}
|
|
238
|
+
return acc;
|
|
239
|
+
}, {});
|
|
240
|
+
// mutate the original group-rank with alphabetized-rank
|
|
241
|
+
for (const importedItem of imported) {
|
|
242
|
+
importedItem.rank =
|
|
243
|
+
alphabetizedRanks[`${importedItem.key}|${importedItem.node.importKind}`];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function makeOutOfOrderReport(context, imported) {
|
|
247
|
+
const outOfOrder = findOutOfOrder(imported);
|
|
248
|
+
if (outOfOrder.length === 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// There are things to report. Try to minimize the number of reported errors.
|
|
252
|
+
const reversedImported = reverse(imported);
|
|
253
|
+
const reversedOrder = findOutOfOrder(reversedImported);
|
|
254
|
+
if (reversedOrder.length < outOfOrder.length) {
|
|
255
|
+
reportOutOfOrder(context, reversedImported, reversedOrder, 'after');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
reportOutOfOrder(context, imported, outOfOrder, 'before');
|
|
259
|
+
}
|
|
260
|
+
function isSimpleObjectPropertyList(properties) {
|
|
261
|
+
return properties.every((p) => {
|
|
262
|
+
if (p.type !== TSESTree.AST_NODE_TYPES.Property) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (p.key.type !== TSESTree.AST_NODE_TYPES.Identifier) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
return p.value.type === TSESTree.AST_NODE_TYPES.Identifier;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
export default createRule({
|
|
272
|
+
meta: {
|
|
273
|
+
type: 'suggestion',
|
|
274
|
+
docs: {
|
|
275
|
+
description: 'Ensure named imports and exports are sorted consistently.',
|
|
276
|
+
},
|
|
277
|
+
fixable: 'code',
|
|
278
|
+
schema: [
|
|
279
|
+
{
|
|
280
|
+
type: 'object',
|
|
281
|
+
properties: {
|
|
282
|
+
ignoreCase: {
|
|
283
|
+
type: 'boolean',
|
|
284
|
+
},
|
|
285
|
+
types: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
enum: ['mixed', 'types-first', 'types-last'],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
additionalProperties: false,
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
messages: {
|
|
294
|
+
order: '{{secondImport}} should occur {{order}} {{firstImport}}',
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
defaultOptions: [
|
|
298
|
+
{
|
|
299
|
+
ignoreCase: false,
|
|
300
|
+
types: 'types-first',
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
create(context, [options]) {
|
|
304
|
+
const namedGroups = options.types === 'mixed'
|
|
305
|
+
? []
|
|
306
|
+
: options.types === 'types-last'
|
|
307
|
+
? ['value']
|
|
308
|
+
: ['type'];
|
|
309
|
+
const ignoreCase = options.ignoreCase ?? false;
|
|
310
|
+
const exportMap = new Map();
|
|
311
|
+
function getBlockExports(node) {
|
|
312
|
+
let blockExports = exportMap.get(node);
|
|
313
|
+
if (!blockExports) {
|
|
314
|
+
exportMap.set(node, (blockExports = []));
|
|
315
|
+
}
|
|
316
|
+
return blockExports;
|
|
317
|
+
}
|
|
318
|
+
function makeNamedOrderReport(context, namedImports) {
|
|
319
|
+
if (namedImports.length > 1) {
|
|
320
|
+
const imports = namedImports.map((namedImport) => {
|
|
321
|
+
const kind = namedImport.kind ?? 'value';
|
|
322
|
+
const rank = namedGroups.indexOf(kind);
|
|
323
|
+
return {
|
|
324
|
+
displayName: namedImport.value,
|
|
325
|
+
key: `${namedImport.value}:${namedImport.alias ?? ''}`,
|
|
326
|
+
rank: rank === -1 ? namedGroups.length : rank,
|
|
327
|
+
...namedImport,
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
mutateRanksToAlphabetize(imports, ignoreCase);
|
|
331
|
+
makeOutOfOrderReport(context, imports);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
ImportDeclaration(node) {
|
|
336
|
+
if (node.specifiers.length === 0) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
makeNamedOrderReport(context, node.specifiers
|
|
340
|
+
.filter((specifier) => specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier)
|
|
341
|
+
.map((specifier) => ({
|
|
342
|
+
node: specifier,
|
|
343
|
+
value: getValue(specifier.imported),
|
|
344
|
+
type: 'import',
|
|
345
|
+
kind: specifier.importKind,
|
|
346
|
+
...(specifier.local.range[0] !== specifier.imported.range[0] && {
|
|
347
|
+
alias: specifier.local.name,
|
|
348
|
+
}),
|
|
349
|
+
})));
|
|
350
|
+
},
|
|
351
|
+
VariableDeclarator(node) {
|
|
352
|
+
if (node.id.type === TSESTree.AST_NODE_TYPES.ObjectPattern &&
|
|
353
|
+
isRequireExpression(node.init)) {
|
|
354
|
+
const { properties } = node.id;
|
|
355
|
+
if (!isSimpleObjectPropertyList(properties)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
makeNamedOrderReport(context, properties.map((prop) => {
|
|
359
|
+
const key = prop.key;
|
|
360
|
+
const value = prop.value;
|
|
361
|
+
return {
|
|
362
|
+
node: prop,
|
|
363
|
+
value: key.name,
|
|
364
|
+
type: 'require',
|
|
365
|
+
...(key.range[0] !== value.range[0] && {
|
|
366
|
+
alias: value.name,
|
|
367
|
+
}),
|
|
368
|
+
};
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
ExportNamedDeclaration(node) {
|
|
373
|
+
makeNamedOrderReport(context, node.specifiers.map((specifier) => ({
|
|
374
|
+
node: specifier,
|
|
375
|
+
value: getValue(specifier.local),
|
|
376
|
+
type: 'export',
|
|
377
|
+
kind: specifier.exportKind,
|
|
378
|
+
...(specifier.local.range[0] !== specifier.exported.range[0] && {
|
|
379
|
+
alias: getValue(specifier.exported),
|
|
380
|
+
}),
|
|
381
|
+
})));
|
|
382
|
+
},
|
|
383
|
+
AssignmentExpression(node) {
|
|
384
|
+
if (node.parent.type === TSESTree.AST_NODE_TYPES.ExpressionStatement) {
|
|
385
|
+
if (isCJSExports(context, node.left)) {
|
|
386
|
+
if (node.right.type === TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
387
|
+
const { properties } = node.right;
|
|
388
|
+
if (!isSimpleObjectPropertyList(properties)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
makeNamedOrderReport(context, properties.map((prop) => {
|
|
392
|
+
const key = prop.key;
|
|
393
|
+
const value = prop.value;
|
|
394
|
+
return {
|
|
395
|
+
node: prop,
|
|
396
|
+
value: key.name,
|
|
397
|
+
type: 'export',
|
|
398
|
+
...(key.range[0] !== value.range[0] && {
|
|
399
|
+
alias: value.name,
|
|
400
|
+
}),
|
|
401
|
+
};
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const nameParts = getNamedCJSExports(context, node.left);
|
|
407
|
+
if (nameParts && nameParts.length > 0) {
|
|
408
|
+
const name = nameParts.join('.');
|
|
409
|
+
getBlockExports(node.parent.parent).push({
|
|
410
|
+
node,
|
|
411
|
+
value: name,
|
|
412
|
+
displayName: name,
|
|
413
|
+
type: 'export',
|
|
414
|
+
rank: 0,
|
|
415
|
+
key: name,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
//endregion types
|
|
425
|
+
//region utils
|
|
426
|
+
function getValue(node) {
|
|
427
|
+
switch (node.type) {
|
|
428
|
+
case TSESTree.AST_NODE_TYPES.Identifier:
|
|
429
|
+
return node.name;
|
|
430
|
+
case TSESTree.AST_NODE_TYPES.Literal:
|
|
431
|
+
return node.value;
|
|
432
|
+
default:
|
|
433
|
+
throw new Error(`Unsupported node type: ${node.type}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
//endregion
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PluginName } from './constants.js';
|
|
2
|
+
export type Arrayable<T> = T | readonly T[];
|
|
3
|
+
export type LiteralNodeValue = string | number | bigint | boolean | RegExp | null;
|
|
4
|
+
type WithPluginName<T extends string | object> = T extends string ? `${PluginName}/${T}` : {
|
|
5
|
+
[K in keyof T as WithPluginName<K & string>]: T[K];
|
|
6
|
+
};
|
|
7
|
+
export type PluginSettings = WithPluginName<{
|
|
8
|
+
tsconfig?: string;
|
|
9
|
+
coreModules?: string[];
|
|
10
|
+
internalRegex?: string;
|
|
11
|
+
}>;
|
|
12
|
+
export {};
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface RankedItem {
|
|
2
|
+
rank: number;
|
|
3
|
+
}
|
|
4
|
+
export declare function reverse<T extends RankedItem>(array: T[]): T[];
|
|
5
|
+
export declare function findOutOfOrder<T extends RankedItem>(array: T[]): T[];
|
|
6
|
+
export declare function groupBy<T>(array: T[], grouper: (item: T, index: number) => string | number): Record<string, T[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function reverse(array) {
|
|
2
|
+
return array.map((item) => ({ ...item, rank: -item.rank })).reverse();
|
|
3
|
+
}
|
|
4
|
+
export function findOutOfOrder(array) {
|
|
5
|
+
if (array.length === 0) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
let maxSeenRankItem = array[0];
|
|
9
|
+
return array.filter((item) => {
|
|
10
|
+
const res = item.rank < maxSeenRankItem.rank;
|
|
11
|
+
if (maxSeenRankItem.rank < item.rank) {
|
|
12
|
+
maxSeenRankItem = item;
|
|
13
|
+
}
|
|
14
|
+
return res;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function groupBy(array, grouper) {
|
|
18
|
+
return array.reduce((acc, item, index) => {
|
|
19
|
+
const key = grouper(item, index);
|
|
20
|
+
(acc[key] ??= []).push(item);
|
|
21
|
+
return acc;
|
|
22
|
+
}, {});
|
|
23
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
export declare function findRootNode(node: TSESTree.Node): TSESTree.Node;
|
|
3
|
+
export declare function findEndOfLineWithComments(sourceCode: TSESLint.SourceCode, node: TSESTree.Node): number;
|
|
4
|
+
export declare function findStartOfLineWithComments(sourceCode: TSESLint.SourceCode, node: TSESTree.Node): number;
|
package/lib/utils/ast.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
export function findRootNode(node) {
|
|
3
|
+
let parent = node;
|
|
4
|
+
while (parent.parent != null &&
|
|
5
|
+
// eslint-disable-next-line eslint-plugin/no-property-in-node
|
|
6
|
+
(!('body' in parent.parent) || parent.parent.body == null)) {
|
|
7
|
+
parent = parent.parent;
|
|
8
|
+
}
|
|
9
|
+
return parent;
|
|
10
|
+
}
|
|
11
|
+
function getTokensOrCommentsAfter(sourceCode, node, count) {
|
|
12
|
+
let currentNodeOrToken = node;
|
|
13
|
+
const result = [];
|
|
14
|
+
for (let i = 0; i < count; i++) {
|
|
15
|
+
currentNodeOrToken = sourceCode.getTokenAfter(currentNodeOrToken, {
|
|
16
|
+
includeComments: true,
|
|
17
|
+
});
|
|
18
|
+
if (currentNodeOrToken == null) {
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
result.push(currentNodeOrToken);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function getTokensOrCommentsBefore(sourceCode, node, count) {
|
|
26
|
+
let currentNodeOrToken = node;
|
|
27
|
+
const result = [];
|
|
28
|
+
for (let i = 0; i < count; i++) {
|
|
29
|
+
currentNodeOrToken = sourceCode.getTokenBefore(currentNodeOrToken, {
|
|
30
|
+
includeComments: true,
|
|
31
|
+
});
|
|
32
|
+
if (currentNodeOrToken == null) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
result.push(currentNodeOrToken);
|
|
36
|
+
}
|
|
37
|
+
return result.reverse();
|
|
38
|
+
}
|
|
39
|
+
function takeTokensAfterWhile(sourceCode, node, condition) {
|
|
40
|
+
const tokens = getTokensOrCommentsAfter(sourceCode, node, 100);
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const token of tokens) {
|
|
43
|
+
if (condition(token)) {
|
|
44
|
+
result.push(token);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
function takeTokensBeforeWhile(sourceCode, node, condition) {
|
|
53
|
+
const tokens = getTokensOrCommentsBefore(sourceCode, node, 100);
|
|
54
|
+
const result = [];
|
|
55
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
56
|
+
if (condition(tokens[i])) {
|
|
57
|
+
result.push(tokens[i]);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result.reverse();
|
|
64
|
+
}
|
|
65
|
+
export function findEndOfLineWithComments(sourceCode, node) {
|
|
66
|
+
const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node));
|
|
67
|
+
const endOfTokens = tokensToEndOfLine.length > 0
|
|
68
|
+
? tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1]
|
|
69
|
+
: node.range[1];
|
|
70
|
+
let result = endOfTokens;
|
|
71
|
+
for (let i = endOfTokens; i < sourceCode.text.length; i++) {
|
|
72
|
+
if (sourceCode.text[i] === '\n') {
|
|
73
|
+
result = i + 1;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (sourceCode.text[i] !== ' ' &&
|
|
77
|
+
sourceCode.text[i] !== '\t' &&
|
|
78
|
+
sourceCode.text[i] !== '\r') {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
result = i + 1;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function commentOnSameLineAs(node) {
|
|
86
|
+
return (token) => (token.type === TSESTree.AST_TOKEN_TYPES.Block ||
|
|
87
|
+
token.type === TSESTree.AST_TOKEN_TYPES.Line) &&
|
|
88
|
+
token.loc.start.line === token.loc.end.line &&
|
|
89
|
+
token.loc.end.line === node.loc.end.line;
|
|
90
|
+
}
|
|
91
|
+
export function findStartOfLineWithComments(sourceCode, node) {
|
|
92
|
+
const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node));
|
|
93
|
+
const startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range[0] : node.range[0];
|
|
94
|
+
let result = startOfTokens;
|
|
95
|
+
for (let i = startOfTokens - 1; i > 0; i--) {
|
|
96
|
+
if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t') {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
result = i;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const createRule = function createRule({ create, defaultOptions, meta }) {
|
|
2
|
+
return {
|
|
3
|
+
meta,
|
|
4
|
+
defaultOptions,
|
|
5
|
+
create(context) {
|
|
6
|
+
const optionsWithDefaults = applyDefaults(defaultOptions, context.options);
|
|
7
|
+
return create(context, optionsWithDefaults);
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
function applyDefaults(defaultOptions, userOptions) {
|
|
12
|
+
// clone defaults
|
|
13
|
+
const options = structuredClone(defaultOptions);
|
|
14
|
+
if (userOptions == null) {
|
|
15
|
+
return options;
|
|
16
|
+
}
|
|
17
|
+
;
|
|
18
|
+
options.forEach((opt, i) => {
|
|
19
|
+
if (userOptions[i] !== undefined) {
|
|
20
|
+
const userOpt = userOptions[i];
|
|
21
|
+
if (isObjectNotArray(userOpt) && isObjectNotArray(opt)) {
|
|
22
|
+
options[i] = deepMerge(opt, userOpt);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
options[i] = userOpt;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return options;
|
|
30
|
+
}
|
|
31
|
+
function isObjectNotArray(obj) {
|
|
32
|
+
return typeof obj === 'object' && obj != null && !Array.isArray(obj);
|
|
33
|
+
}
|
|
34
|
+
function deepMerge(first, second) {
|
|
35
|
+
const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
|
|
36
|
+
return Object.fromEntries([...keys].map((key) => {
|
|
37
|
+
const firstHasKey = key in first;
|
|
38
|
+
const secondHasKey = key in second;
|
|
39
|
+
const firstValue = first[key];
|
|
40
|
+
const secondValue = second[key];
|
|
41
|
+
let value;
|
|
42
|
+
if (firstHasKey && secondHasKey) {
|
|
43
|
+
if (isObjectNotArray(firstValue) && isObjectNotArray(secondValue)) {
|
|
44
|
+
value = deepMerge(firstValue, secondValue);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
value = secondValue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (firstHasKey) {
|
|
51
|
+
value = firstValue;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
value = secondValue;
|
|
55
|
+
}
|
|
56
|
+
return [key, value];
|
|
57
|
+
}));
|
|
58
|
+
}
|