@so1ve/eslint-plugin-sort-imports 0.121.3 → 0.122.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/package.json +1 -1
- package/src/exports.js +103 -103
- package/src/imports.js +150 -150
- package/src/index.js +11 -11
- package/src/shared.js +887 -887
package/src/shared.js
CHANGED
|
@@ -1,887 +1,887 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const natsort = require("natsort").default;
|
|
4
|
-
|
|
5
|
-
const NEWLINE = /(\r?\n)/;
|
|
6
|
-
|
|
7
|
-
const hasNewline = (string) => NEWLINE.test(string);
|
|
8
|
-
|
|
9
|
-
function guessNewline(sourceCode) {
|
|
10
|
-
const match = NEWLINE.exec(sourceCode.text);
|
|
11
|
-
|
|
12
|
-
return match == null ? "\n" : match[0];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function parseWhitespace(whitespace) {
|
|
16
|
-
const allItems = whitespace.split(NEWLINE);
|
|
17
|
-
|
|
18
|
-
// Remove blank lines. `allItems` contains alternating `spaces` (which can be
|
|
19
|
-
// the empty string) and `newline` (which is either "\r\n" or "\n"). So in
|
|
20
|
-
// practice `allItems` grows like this as there are more newlines in
|
|
21
|
-
// `whitespace`:
|
|
22
|
-
//
|
|
23
|
-
// [spaces]
|
|
24
|
-
// [spaces, newline, spaces]
|
|
25
|
-
// [spaces, newline, spaces, newline, spaces]
|
|
26
|
-
// [spaces, newline, spaces, newline, spaces, newline, spaces]
|
|
27
|
-
//
|
|
28
|
-
// If there are 5 or more items we have at least one blank line. If so, keep
|
|
29
|
-
// the first `spaces`, the first `newline` and the last `spaces`.
|
|
30
|
-
const items =
|
|
31
|
-
allItems.length >= 5
|
|
32
|
-
? [...allItems.slice(0, 2), ...allItems.slice(-1)]
|
|
33
|
-
: allItems;
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
items
|
|
37
|
-
.map((spacesOrNewline, index) =>
|
|
38
|
-
index % 2 === 0
|
|
39
|
-
? { type: "Spaces", code: spacesOrNewline }
|
|
40
|
-
: { type: "Newline", code: spacesOrNewline },
|
|
41
|
-
)
|
|
42
|
-
// Remove empty spaces since it makes debugging easier.
|
|
43
|
-
.filter((token) => token.code !== "")
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const naturalSort = natsort();
|
|
48
|
-
function compare(path1, path2) {
|
|
49
|
-
const path1Depth = path1.split("-").filter((p) => p === "__").length;
|
|
50
|
-
const path2Depth = path2.split("-").filter((p) => p === "__").length;
|
|
51
|
-
const path1IsDot = path1 === "_-,";
|
|
52
|
-
const path2IsDot = path2 === "_-,";
|
|
53
|
-
|
|
54
|
-
if (path1IsDot && !path2IsDot) {
|
|
55
|
-
return 1;
|
|
56
|
-
}
|
|
57
|
-
if (path2IsDot && !path1IsDot) {
|
|
58
|
-
return -1;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return path1Depth === path2Depth
|
|
62
|
-
? naturalSort(path1, path2)
|
|
63
|
-
: path2Depth - path1Depth;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const isIdentifier = (node) => node.type === "Identifier";
|
|
67
|
-
|
|
68
|
-
const isKeyword = (node) => node.type === "Keyword";
|
|
69
|
-
|
|
70
|
-
const isPunctuator = (node, value) =>
|
|
71
|
-
node.type === "Punctuator" && node.value === value;
|
|
72
|
-
|
|
73
|
-
const isBlockComment = (node) => node.type === "Block";
|
|
74
|
-
|
|
75
|
-
const isLineComment = (node) => node.type === "Line";
|
|
76
|
-
|
|
77
|
-
const isSpaces = (node) => node.type === "Spaces";
|
|
78
|
-
|
|
79
|
-
const isNewline = (node) => node.type === "Newline";
|
|
80
|
-
|
|
81
|
-
const getImportExportKind = (node) =>
|
|
82
|
-
node.importKind || node.exportKind || "value";
|
|
83
|
-
|
|
84
|
-
function getSource(node) {
|
|
85
|
-
const source = node.source.value;
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
// Sort by directory level rather than by string length.
|
|
89
|
-
source: source
|
|
90
|
-
// Treat `.` as `./`, `..` as `../`, `../..` as `../../` etc.
|
|
91
|
-
.replace(/^[./]*\.$/, "$&/")
|
|
92
|
-
// Make `../` sort after `../../` but before `../a` etc.
|
|
93
|
-
// Why a comma? See the next comment.
|
|
94
|
-
.replace(/^[./]*\/$/, "$&,")
|
|
95
|
-
// Make `.` and `/` sort before any other punctation.
|
|
96
|
-
// The default order is: _ - , x x x . x x x / x x x
|
|
97
|
-
// We’re changing it to: . / , x x x _ x x x - x x x
|
|
98
|
-
.replace(/[./_-]/g, (char) => {
|
|
99
|
-
switch (char) {
|
|
100
|
-
case ".": {
|
|
101
|
-
return "_";
|
|
102
|
-
}
|
|
103
|
-
case "/": {
|
|
104
|
-
return "-";
|
|
105
|
-
}
|
|
106
|
-
case "_": {
|
|
107
|
-
return ".";
|
|
108
|
-
}
|
|
109
|
-
case "-": {
|
|
110
|
-
return "/";
|
|
111
|
-
}
|
|
112
|
-
// istanbul ignore next
|
|
113
|
-
default: {
|
|
114
|
-
throw new Error(`Unknown source substitution character: ${char}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}),
|
|
118
|
-
originalSource: source,
|
|
119
|
-
kind: getImportExportKind(node),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Like `Array.prototype.findIndex`, but searches from the end.
|
|
124
|
-
function findLastIndex(array, fn) {
|
|
125
|
-
for (let index = array.length - 1; index >= 0; index--) {
|
|
126
|
-
if (fn(array[index], index, array)) {
|
|
127
|
-
return index;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// There are currently no usages of `findLastIndex` where nothing is found.
|
|
132
|
-
// istanbul ignore next
|
|
133
|
-
return -1;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Like `Array.prototype.flatMap`, had it been available.
|
|
137
|
-
const flatMap = (array, fn) => array.flatMap(fn);
|
|
138
|
-
|
|
139
|
-
// Returns `sourceCode.getTokens(node)` plus whitespace and comments. All tokens
|
|
140
|
-
// have a `code` property with `sourceCode.getText(token)`.
|
|
141
|
-
function getAllTokens(node, sourceCode) {
|
|
142
|
-
const tokens = sourceCode.getTokens(node);
|
|
143
|
-
const lastTokenIndex = tokens.length - 1;
|
|
144
|
-
|
|
145
|
-
return flatMap(tokens, (token, tokenIndex) => {
|
|
146
|
-
const newToken = { ...token, code: sourceCode.getText(token) };
|
|
147
|
-
|
|
148
|
-
if (tokenIndex === lastTokenIndex) {
|
|
149
|
-
return [newToken];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const comments = sourceCode.getCommentsAfter(token);
|
|
153
|
-
const last = comments.length > 0 ? comments[comments.length - 1] : token;
|
|
154
|
-
const nextToken = tokens[tokenIndex + 1];
|
|
155
|
-
|
|
156
|
-
return [
|
|
157
|
-
newToken,
|
|
158
|
-
...flatMap(comments, (comment, commentIndex) => {
|
|
159
|
-
const previous =
|
|
160
|
-
commentIndex === 0 ? token : comments[commentIndex - 1];
|
|
161
|
-
|
|
162
|
-
return [
|
|
163
|
-
...parseWhitespace(
|
|
164
|
-
sourceCode.text.slice(previous.range[1], comment.range[0]),
|
|
165
|
-
),
|
|
166
|
-
{ ...comment, code: sourceCode.getText(comment) },
|
|
167
|
-
];
|
|
168
|
-
}),
|
|
169
|
-
...parseWhitespace(
|
|
170
|
-
sourceCode.text.slice(last.range[1], nextToken.range[0]),
|
|
171
|
-
),
|
|
172
|
-
];
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Prints tokens that are enhanced with a `code` property – like those returned
|
|
177
|
-
// by `getAllTokens` and `parseWhitespace`.
|
|
178
|
-
const printTokens = (tokens) => tokens.map((token) => token.code).join("");
|
|
179
|
-
|
|
180
|
-
const removeBlankLines = (whitespace) =>
|
|
181
|
-
printTokens(parseWhitespace(whitespace));
|
|
182
|
-
|
|
183
|
-
// `comments` is a list of comments that occur before `node`. Print those and
|
|
184
|
-
// the whitespace between themselves and between `node`.
|
|
185
|
-
function printCommentsBefore(node, comments, sourceCode) {
|
|
186
|
-
const lastIndex = comments.length - 1;
|
|
187
|
-
|
|
188
|
-
return comments
|
|
189
|
-
.map((comment, index) => {
|
|
190
|
-
const next = index === lastIndex ? node : comments[index + 1];
|
|
191
|
-
|
|
192
|
-
return (
|
|
193
|
-
sourceCode.getText(comment) +
|
|
194
|
-
removeBlankLines(sourceCode.text.slice(comment.range[1], next.range[0]))
|
|
195
|
-
);
|
|
196
|
-
})
|
|
197
|
-
.join("");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// `comments` is a list of comments that occur after `node`. Print those and
|
|
201
|
-
// the whitespace between themselves and between `node`.
|
|
202
|
-
const printCommentsAfter = (node, comments, sourceCode) =>
|
|
203
|
-
comments
|
|
204
|
-
.map((comment, index) => {
|
|
205
|
-
const previous = index === 0 ? node : comments[index - 1];
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
removeBlankLines(
|
|
209
|
-
sourceCode.text.slice(previous.range[1], comment.range[0]),
|
|
210
|
-
) + sourceCode.getText(comment)
|
|
211
|
-
);
|
|
212
|
-
})
|
|
213
|
-
.join("");
|
|
214
|
-
|
|
215
|
-
function getIndentation(node, sourceCode) {
|
|
216
|
-
const tokenBefore = sourceCode.getTokenBefore(node, {
|
|
217
|
-
includeComments: true,
|
|
218
|
-
});
|
|
219
|
-
if (tokenBefore == null) {
|
|
220
|
-
const text = sourceCode.text.slice(0, node.range[0]);
|
|
221
|
-
const lines = text.split(NEWLINE);
|
|
222
|
-
|
|
223
|
-
return lines[lines.length - 1];
|
|
224
|
-
}
|
|
225
|
-
const text = sourceCode.text.slice(tokenBefore.range[1], node.range[0]);
|
|
226
|
-
const lines = text.split(NEWLINE);
|
|
227
|
-
|
|
228
|
-
return lines.length > 1 ? lines[lines.length - 1] : "";
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function getTrailingSpaces(node, sourceCode) {
|
|
232
|
-
const tokenAfter = sourceCode.getTokenAfter(node, {
|
|
233
|
-
includeComments: true,
|
|
234
|
-
});
|
|
235
|
-
if (tokenAfter == null) {
|
|
236
|
-
const text = sourceCode.text.slice(node.range[1]);
|
|
237
|
-
const lines = text.split(NEWLINE);
|
|
238
|
-
|
|
239
|
-
return lines[0];
|
|
240
|
-
}
|
|
241
|
-
const text = sourceCode.text.slice(node.range[1], tokenAfter.range[0]);
|
|
242
|
-
const lines = text.split(NEWLINE);
|
|
243
|
-
|
|
244
|
-
return lines[0];
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const sortImportExportItems = (items) =>
|
|
248
|
-
[...items].sort((itemA, itemB) =>
|
|
249
|
-
// If both items are side effect imports, keep their original order.
|
|
250
|
-
itemA.isSideEffectImport && itemB.isSideEffectImport
|
|
251
|
-
? itemA.index - itemB.index
|
|
252
|
-
: // If one of the items is a side effect import, move it first.
|
|
253
|
-
itemA.isSideEffectImport
|
|
254
|
-
? -1
|
|
255
|
-
: itemB.isSideEffectImport
|
|
256
|
-
? 1
|
|
257
|
-
: // Compare the `from` part.
|
|
258
|
-
compare(itemA.source.source, itemB.source.source) ||
|
|
259
|
-
// The `.source` has been slightly tweaked. To stay fully deterministic,
|
|
260
|
-
// also sort on the original value.
|
|
261
|
-
compare(itemA.source.originalSource, itemB.source.originalSource) ||
|
|
262
|
-
// Then put type imports/exports before regular ones.
|
|
263
|
-
compare(itemA.source.kind, itemB.source.kind) ||
|
|
264
|
-
// Keep the original order if the sources are the same. It’s not worth
|
|
265
|
-
// trying to compare anything else, and you can use `import/no-duplicates`
|
|
266
|
-
// to get rid of the problem anyway.
|
|
267
|
-
itemA.index - itemB.index,
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const sortSpecifierItems = (items) =>
|
|
271
|
-
[...items].sort(
|
|
272
|
-
(itemA, itemB) =>
|
|
273
|
-
// Compare by imported or exported name (external interface name).
|
|
274
|
-
// import { a as b } from "a"
|
|
275
|
-
// ^
|
|
276
|
-
// export { b as a }
|
|
277
|
-
// ^
|
|
278
|
-
compare(
|
|
279
|
-
(itemA.node.imported || itemA.node.exported).name,
|
|
280
|
-
(itemB.node.imported || itemB.node.exported).name,
|
|
281
|
-
) ||
|
|
282
|
-
// Then compare by the file-local name.
|
|
283
|
-
// import { a as b } from "a"
|
|
284
|
-
// ^
|
|
285
|
-
// export { b as a }
|
|
286
|
-
// ^
|
|
287
|
-
compare(itemA.node.local.name, itemB.node.local.name) ||
|
|
288
|
-
// Then put type specifiers before regular ones.
|
|
289
|
-
compare(
|
|
290
|
-
getImportExportKind(itemA.node),
|
|
291
|
-
getImportExportKind(itemB.node),
|
|
292
|
-
) ||
|
|
293
|
-
// Keep the original order if the names are the same. It’s not worth
|
|
294
|
-
// trying to compare anything else, `import {a, a} from "mod"` is a syntax
|
|
295
|
-
// error anyway (but @babel/eslint-parser kind of supports it).
|
|
296
|
-
// istanbul ignore next
|
|
297
|
-
itemA.index - itemB.index,
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
// A “chunk” is a sequence of statements of a certain type with only comments
|
|
301
|
-
// and whitespace between.
|
|
302
|
-
function extractChunks(parentNode, isPartOfChunk) {
|
|
303
|
-
const chunks = [];
|
|
304
|
-
let chunk = [];
|
|
305
|
-
let lastNode;
|
|
306
|
-
|
|
307
|
-
for (const node of parentNode.body) {
|
|
308
|
-
const result = isPartOfChunk(node, lastNode);
|
|
309
|
-
switch (result) {
|
|
310
|
-
case "PartOfChunk": {
|
|
311
|
-
chunk.push(node);
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
case "PartOfNewChunk": {
|
|
316
|
-
if (chunk.length > 0) {
|
|
317
|
-
chunks.push(chunk);
|
|
318
|
-
}
|
|
319
|
-
chunk = [node];
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
case "NotPartOfChunk": {
|
|
324
|
-
if (chunk.length > 0) {
|
|
325
|
-
chunks.push(chunk);
|
|
326
|
-
chunk = [];
|
|
327
|
-
}
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// istanbul ignore next
|
|
332
|
-
default: {
|
|
333
|
-
throw new Error(`Unknown chunk result: ${result}`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
lastNode = node;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (chunk.length > 0) {
|
|
341
|
-
chunks.push(chunk);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return chunks;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function maybeReportSorting(context, sorted, start, end) {
|
|
348
|
-
const sourceCode = context.getSourceCode();
|
|
349
|
-
const original = sourceCode.getText().slice(start, end);
|
|
350
|
-
if (original !== sorted) {
|
|
351
|
-
context.report({
|
|
352
|
-
messageId: "sort",
|
|
353
|
-
loc: {
|
|
354
|
-
start: sourceCode.getLocFromIndex(start),
|
|
355
|
-
end: sourceCode.getLocFromIndex(end),
|
|
356
|
-
},
|
|
357
|
-
fix: (fixer) => fixer.replaceTextRange([start, end], sorted),
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function printSortedItems(sortedItems, originalItems, sourceCode) {
|
|
363
|
-
const newline = guessNewline(sourceCode);
|
|
364
|
-
|
|
365
|
-
const sorted = sortedItems
|
|
366
|
-
.map((groups) =>
|
|
367
|
-
groups
|
|
368
|
-
.map((groupItems) => groupItems.map((item) => item.code).join(newline))
|
|
369
|
-
.join(newline),
|
|
370
|
-
)
|
|
371
|
-
.join(newline + newline);
|
|
372
|
-
|
|
373
|
-
// Edge case: If the last import/export (after sorting) ends with a line
|
|
374
|
-
// comment and there’s code (or a multiline block comment) on the same line,
|
|
375
|
-
// add a newline so we don’t accidentally comment stuff out.
|
|
376
|
-
const flattened = flatMap(sortedItems, (groups) => groups.flat());
|
|
377
|
-
const lastSortedItem = flattened[flattened.length - 1];
|
|
378
|
-
const lastOriginalItem = originalItems[originalItems.length - 1];
|
|
379
|
-
const nextToken = lastSortedItem.needsNewline
|
|
380
|
-
? sourceCode.getTokenAfter(lastOriginalItem.node, {
|
|
381
|
-
includeComments: true,
|
|
382
|
-
filter: (token) =>
|
|
383
|
-
!isLineComment(token) &&
|
|
384
|
-
!(
|
|
385
|
-
isBlockComment(token) &&
|
|
386
|
-
token.loc.end.line === lastOriginalItem.node.loc.end.line
|
|
387
|
-
),
|
|
388
|
-
})
|
|
389
|
-
: undefined;
|
|
390
|
-
const maybeNewline =
|
|
391
|
-
nextToken != null &&
|
|
392
|
-
nextToken.loc.start.line === lastOriginalItem.node.loc.end.line
|
|
393
|
-
? newline
|
|
394
|
-
: "";
|
|
395
|
-
|
|
396
|
-
return sorted + maybeNewline;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Wrap the import/export nodes in `passedChunk` in objects with more data about
|
|
400
|
-
// the import/export. Most importantly there’s a `code` property that contains
|
|
401
|
-
// the node as a string, with comments (if any). Finding the corresponding
|
|
402
|
-
// comments is the hard part.
|
|
403
|
-
function getImportExportItems(
|
|
404
|
-
passedChunk,
|
|
405
|
-
sourceCode,
|
|
406
|
-
isSideEffectImport,
|
|
407
|
-
getSpecifiers,
|
|
408
|
-
) {
|
|
409
|
-
const chunk = handleLastSemicolon(passedChunk, sourceCode);
|
|
410
|
-
|
|
411
|
-
return chunk.map((node, nodeIndex) => {
|
|
412
|
-
const lastLine =
|
|
413
|
-
nodeIndex === 0
|
|
414
|
-
? node.loc.start.line - 1
|
|
415
|
-
: chunk[nodeIndex - 1].loc.end.line;
|
|
416
|
-
|
|
417
|
-
// Get all comments before the import/export, except:
|
|
418
|
-
//
|
|
419
|
-
// - Comments on another line for the first import/export.
|
|
420
|
-
// - Comments that belong to the previous import/export (if any) – that is,
|
|
421
|
-
// comments that are on the same line as the previous import/export. But
|
|
422
|
-
// multiline block comments always belong to this import/export, not the
|
|
423
|
-
// previous.
|
|
424
|
-
const commentsBefore = sourceCode
|
|
425
|
-
.getCommentsBefore(node)
|
|
426
|
-
.filter(
|
|
427
|
-
(comment) =>
|
|
428
|
-
comment.loc.start.line <= node.loc.start.line &&
|
|
429
|
-
comment.loc.end.line > lastLine &&
|
|
430
|
-
(nodeIndex > 0 || comment.loc.start.line > lastLine),
|
|
431
|
-
);
|
|
432
|
-
|
|
433
|
-
// Get all comments after the import/export that are on the same line.
|
|
434
|
-
// Multiline block comments belong to the _next_ import/export (or the
|
|
435
|
-
// following code in case of the last import/export).
|
|
436
|
-
const commentsAfter = sourceCode
|
|
437
|
-
.getCommentsAfter(node)
|
|
438
|
-
.filter((comment) => comment.loc.end.line === node.loc.end.line);
|
|
439
|
-
|
|
440
|
-
const before = printCommentsBefore(node, commentsBefore, sourceCode);
|
|
441
|
-
const after = printCommentsAfter(node, commentsAfter, sourceCode);
|
|
442
|
-
|
|
443
|
-
// Print the indentation before the import/export or its first comment, if
|
|
444
|
-
// any, to support indentation in `<script>` tags.
|
|
445
|
-
const indentation = getIndentation(
|
|
446
|
-
commentsBefore.length > 0 ? commentsBefore[0] : node,
|
|
447
|
-
sourceCode,
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
// Print spaces after the import/export or its last comment, if any, to
|
|
451
|
-
// avoid producing a sort error just because you accidentally added a few
|
|
452
|
-
// trailing spaces among the imports/exports.
|
|
453
|
-
const trailingSpaces = getTrailingSpaces(
|
|
454
|
-
commentsAfter.length > 0 ? commentsAfter[commentsAfter.length - 1] : node,
|
|
455
|
-
sourceCode,
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
const code =
|
|
459
|
-
indentation +
|
|
460
|
-
before +
|
|
461
|
-
printWithSortedSpecifiers(node, sourceCode, getSpecifiers) +
|
|
462
|
-
after +
|
|
463
|
-
trailingSpaces;
|
|
464
|
-
|
|
465
|
-
const all = [...commentsBefore, node, ...commentsAfter];
|
|
466
|
-
const [start] = all[0].range;
|
|
467
|
-
const [, end] = all[all.length - 1].range;
|
|
468
|
-
|
|
469
|
-
const source = getSource(node);
|
|
470
|
-
|
|
471
|
-
return {
|
|
472
|
-
node,
|
|
473
|
-
code,
|
|
474
|
-
start: start - indentation.length,
|
|
475
|
-
end: end + trailingSpaces.length,
|
|
476
|
-
isSideEffectImport: isSideEffectImport(node, sourceCode),
|
|
477
|
-
source,
|
|
478
|
-
index: nodeIndex,
|
|
479
|
-
needsNewline:
|
|
480
|
-
commentsAfter.length > 0 &&
|
|
481
|
-
isLineComment(commentsAfter[commentsAfter.length - 1]),
|
|
482
|
-
};
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Parsers think that a semicolon after a statement belongs to that statement.
|
|
487
|
-
// But in a semicolon-free code style it might belong to the next statement:
|
|
488
|
-
//
|
|
489
|
-
// import x from "x"
|
|
490
|
-
// ;[].forEach()
|
|
491
|
-
//
|
|
492
|
-
// If the last import/export of a chunk ends with a semicolon, and that
|
|
493
|
-
// semicolon isn’t located on the same line as the `from` string, adjust the
|
|
494
|
-
// node to end at the `from` string instead.
|
|
495
|
-
//
|
|
496
|
-
// In the above example, the import is adjusted to end after `"x"`.
|
|
497
|
-
function handleLastSemicolon(chunk, sourceCode) {
|
|
498
|
-
const lastIndex = chunk.length - 1;
|
|
499
|
-
const lastNode = chunk[lastIndex];
|
|
500
|
-
const [nextToLastToken, lastToken] = sourceCode.getLastTokens(lastNode, {
|
|
501
|
-
count: 2,
|
|
502
|
-
});
|
|
503
|
-
const lastIsSemicolon = isPunctuator(lastToken, ";");
|
|
504
|
-
|
|
505
|
-
if (!lastIsSemicolon) {
|
|
506
|
-
return chunk;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const semicolonBelongsToNode =
|
|
510
|
-
nextToLastToken.loc.end.line === lastToken.loc.start.line ||
|
|
511
|
-
// If there’s no more code after the last import/export the semicolon has to
|
|
512
|
-
// belong to the import/export, even if it is not on the same line.
|
|
513
|
-
sourceCode.getTokenAfter(lastToken) == null;
|
|
514
|
-
|
|
515
|
-
if (semicolonBelongsToNode) {
|
|
516
|
-
return chunk;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Preserve the start position, but use the end position of the `from` string.
|
|
520
|
-
const newLastNode = {
|
|
521
|
-
...lastNode,
|
|
522
|
-
range: [lastNode.range[0], nextToLastToken.range[1]],
|
|
523
|
-
loc: {
|
|
524
|
-
start: lastNode.loc.start,
|
|
525
|
-
end: nextToLastToken.loc.end,
|
|
526
|
-
},
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
return [...chunk.slice(0, lastIndex), newLastNode];
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function printWithSortedSpecifiers(node, sourceCode, getSpecifiers) {
|
|
533
|
-
const allTokens = getAllTokens(node, sourceCode);
|
|
534
|
-
const openBraceIndex = allTokens.findIndex((token) =>
|
|
535
|
-
isPunctuator(token, "{"),
|
|
536
|
-
);
|
|
537
|
-
const closeBraceIndex = allTokens.findIndex((token) =>
|
|
538
|
-
isPunctuator(token, "}"),
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
const specifiers = getSpecifiers(node);
|
|
542
|
-
|
|
543
|
-
if (
|
|
544
|
-
openBraceIndex === -1 ||
|
|
545
|
-
closeBraceIndex === -1 ||
|
|
546
|
-
specifiers.length <= 1
|
|
547
|
-
) {
|
|
548
|
-
return printTokens(allTokens);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const specifierTokens = allTokens.slice(openBraceIndex + 1, closeBraceIndex);
|
|
552
|
-
const itemsResult = getSpecifierItems(specifierTokens, sourceCode);
|
|
553
|
-
|
|
554
|
-
const items = itemsResult.items.map((originalItem, index) => ({
|
|
555
|
-
...originalItem,
|
|
556
|
-
node: specifiers[index],
|
|
557
|
-
}));
|
|
558
|
-
|
|
559
|
-
const sortedItems = sortSpecifierItems(items);
|
|
560
|
-
|
|
561
|
-
const newline = guessNewline(sourceCode);
|
|
562
|
-
|
|
563
|
-
// `allTokens[closeBraceIndex - 1]` wouldn’t work because `allTokens` contains
|
|
564
|
-
// comments and whitespace.
|
|
565
|
-
const hasTrailingComma = isPunctuator(
|
|
566
|
-
sourceCode.getTokenBefore(allTokens[closeBraceIndex]),
|
|
567
|
-
",",
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
const lastIndex = sortedItems.length - 1;
|
|
571
|
-
const sorted = flatMap(sortedItems, (item, index) => {
|
|
572
|
-
const previous = index === 0 ? undefined : sortedItems[index - 1];
|
|
573
|
-
|
|
574
|
-
// Add a newline if the item needs one, unless the previous item (if any)
|
|
575
|
-
// already ends with a newline.
|
|
576
|
-
const maybeNewline =
|
|
577
|
-
previous != null &&
|
|
578
|
-
needsStartingNewline(item.before) &&
|
|
579
|
-
!(
|
|
580
|
-
previous.after.length > 0 &&
|
|
581
|
-
isNewline(previous.after[previous.after.length - 1])
|
|
582
|
-
)
|
|
583
|
-
? [{ type: "Newline", code: newline }]
|
|
584
|
-
: [];
|
|
585
|
-
|
|
586
|
-
if (index < lastIndex || hasTrailingComma) {
|
|
587
|
-
return [
|
|
588
|
-
...maybeNewline,
|
|
589
|
-
...item.before,
|
|
590
|
-
...item.specifier,
|
|
591
|
-
{ type: "Comma", code: "," },
|
|
592
|
-
...item.after,
|
|
593
|
-
];
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const nonBlankIndex = item.after.findIndex(
|
|
597
|
-
(token) => !isNewline(token) && !isSpaces(token),
|
|
598
|
-
);
|
|
599
|
-
|
|
600
|
-
// Remove whitespace and newlines at the start of `.after` if the item had a
|
|
601
|
-
// comma before, but now hasn’t to avoid blank lines and excessive
|
|
602
|
-
// whitespace before `}`.
|
|
603
|
-
const after = item.hadComma
|
|
604
|
-
? nonBlankIndex === -1
|
|
605
|
-
? []
|
|
606
|
-
: item.after.slice(nonBlankIndex)
|
|
607
|
-
: item.after;
|
|
608
|
-
|
|
609
|
-
return [...maybeNewline, ...item.before, ...item.specifier, ...after];
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
const maybeNewline =
|
|
613
|
-
needsStartingNewline(itemsResult.after) &&
|
|
614
|
-
!isNewline(sorted[sorted.length - 1])
|
|
615
|
-
? [{ type: "Newline", code: newline }]
|
|
616
|
-
: [];
|
|
617
|
-
|
|
618
|
-
return printTokens([
|
|
619
|
-
...allTokens.slice(0, openBraceIndex + 1),
|
|
620
|
-
...itemsResult.before,
|
|
621
|
-
...sorted,
|
|
622
|
-
...maybeNewline,
|
|
623
|
-
...itemsResult.after,
|
|
624
|
-
...allTokens.slice(closeBraceIndex),
|
|
625
|
-
]);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const makeEmptyItem = () => ({
|
|
629
|
-
// "before" | "specifier" | "after"
|
|
630
|
-
state: "before",
|
|
631
|
-
before: [],
|
|
632
|
-
after: [],
|
|
633
|
-
specifier: [],
|
|
634
|
-
hadComma: false,
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
// Turns a list of tokens between the `{` and `}` of an import/export specifiers
|
|
638
|
-
// list into an object with the following properties:
|
|
639
|
-
//
|
|
640
|
-
// - before: Array of tokens – whitespace and comments after the `{` that do not
|
|
641
|
-
// belong to any specifier.
|
|
642
|
-
// - after: Array of tokens – whitespace and comments before the `}` that do not
|
|
643
|
-
// belong to any specifier.
|
|
644
|
-
// - items: Array of specifier items.
|
|
645
|
-
//
|
|
646
|
-
// Each specifier item looks like this:
|
|
647
|
-
//
|
|
648
|
-
// - before: Array of tokens – whitespace and comments before the specifier.
|
|
649
|
-
// - after: Array of tokens – whitespace and comments after the specifier.
|
|
650
|
-
// - specifier: Array of tokens – identifiers, whitespace and comments of the
|
|
651
|
-
// specifier.
|
|
652
|
-
// - hadComma: A Boolean representing if the specifier had a comma originally.
|
|
653
|
-
//
|
|
654
|
-
// We have to do carefully preserve all original whitespace this way in order to
|
|
655
|
-
// be compatible with other stylistic ESLint rules.
|
|
656
|
-
function getSpecifierItems(tokens) {
|
|
657
|
-
const result = {
|
|
658
|
-
before: [],
|
|
659
|
-
after: [],
|
|
660
|
-
items: [],
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
let current = makeEmptyItem();
|
|
664
|
-
|
|
665
|
-
for (const token of tokens) {
|
|
666
|
-
switch (current.state) {
|
|
667
|
-
case "before": {
|
|
668
|
-
switch (token.type) {
|
|
669
|
-
case "Newline": {
|
|
670
|
-
current.before.push(token);
|
|
671
|
-
|
|
672
|
-
// All whitespace and comments before the first newline or
|
|
673
|
-
// identifier belong to the `{`, not the first specifier.
|
|
674
|
-
if (result.before.length === 0 && result.items.length === 0) {
|
|
675
|
-
result.before = current.before;
|
|
676
|
-
current = makeEmptyItem();
|
|
677
|
-
}
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
case "Spaces":
|
|
682
|
-
case "Block":
|
|
683
|
-
case "Line": {
|
|
684
|
-
current.before.push(token);
|
|
685
|
-
break;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// We’ve reached an identifier.
|
|
689
|
-
default: {
|
|
690
|
-
// All whitespace and comments before the first newline or
|
|
691
|
-
// identifier belong to the `{`, not the first specifier.
|
|
692
|
-
if (result.before.length === 0 && result.items.length === 0) {
|
|
693
|
-
result.before = current.before;
|
|
694
|
-
current = makeEmptyItem();
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
current.state = "specifier";
|
|
698
|
-
current.specifier.push(token);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
break;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
case "specifier": {
|
|
705
|
-
switch (token.type) {
|
|
706
|
-
case "Punctuator": {
|
|
707
|
-
// There can only be comma punctuators, but future-proof by checking.
|
|
708
|
-
// istanbul ignore else
|
|
709
|
-
if (isPunctuator(token, ",")) {
|
|
710
|
-
current.hadComma = true;
|
|
711
|
-
current.state = "after";
|
|
712
|
-
} else {
|
|
713
|
-
current.specifier.push(token);
|
|
714
|
-
}
|
|
715
|
-
break;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// When consuming the specifier part, we eat every token until a comma
|
|
719
|
-
// or to the end, basically.
|
|
720
|
-
default: {
|
|
721
|
-
current.specifier.push(token);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
case "after": {
|
|
728
|
-
switch (token.type) {
|
|
729
|
-
// Only whitespace and comments after a specifier that are on the same
|
|
730
|
-
// belong to the specifier.
|
|
731
|
-
case "Newline": {
|
|
732
|
-
current.after.push(token);
|
|
733
|
-
result.items.push(current);
|
|
734
|
-
current = makeEmptyItem();
|
|
735
|
-
break;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
case "Spaces":
|
|
739
|
-
case "Line": {
|
|
740
|
-
current.after.push(token);
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
case "Block": {
|
|
745
|
-
// Multiline block comments belong to the next specifier.
|
|
746
|
-
if (hasNewline(token.code)) {
|
|
747
|
-
result.items.push(current);
|
|
748
|
-
current = makeEmptyItem();
|
|
749
|
-
current.before.push(token);
|
|
750
|
-
} else {
|
|
751
|
-
current.after.push(token);
|
|
752
|
-
}
|
|
753
|
-
break;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// We’ve reached another specifier – time to process that one.
|
|
757
|
-
default: {
|
|
758
|
-
result.items.push(current);
|
|
759
|
-
current = makeEmptyItem();
|
|
760
|
-
current.state = "specifier";
|
|
761
|
-
current.specifier.push(token);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// istanbul ignore next
|
|
768
|
-
default: {
|
|
769
|
-
throw new Error(`Unknown state: ${current.state}`);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// We’ve reached the end of the tokens. Handle what’s currently in `current`.
|
|
775
|
-
switch (current.state) {
|
|
776
|
-
// If the last specifier has a trailing comma and some of the remaining
|
|
777
|
-
// whitespace and comments are on the same line we end up here. If so we
|
|
778
|
-
// want to put that whitespace and comments in `result.after`.
|
|
779
|
-
case "before": {
|
|
780
|
-
result.after = current.before;
|
|
781
|
-
break;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// If the last specifier has no trailing comma we end up here. Move all
|
|
785
|
-
// trailing comments and whitespace from `.specifier` to `.after`, and
|
|
786
|
-
// comments and whitespace that don’t belong to the specifier to
|
|
787
|
-
// `result.after`. The last non-comment and non-whitespace token is usually
|
|
788
|
-
// an identifier, but in this case it’s a keyword:
|
|
789
|
-
//
|
|
790
|
-
// export { z, d as default } from "a"
|
|
791
|
-
case "specifier": {
|
|
792
|
-
const lastIdentifierIndex = findLastIndex(
|
|
793
|
-
current.specifier,
|
|
794
|
-
(token2) => isIdentifier(token2) || isKeyword(token2),
|
|
795
|
-
);
|
|
796
|
-
|
|
797
|
-
const specifier = current.specifier.slice(0, lastIdentifierIndex + 1);
|
|
798
|
-
const after = current.specifier.slice(lastIdentifierIndex + 1);
|
|
799
|
-
|
|
800
|
-
// If there’s a newline, put everything up to and including (hence the `+
|
|
801
|
-
// 1`) that newline in the specifiers’s `.after`.
|
|
802
|
-
const newlineIndexRaw = after.findIndex((token2) => isNewline(token2));
|
|
803
|
-
const newlineIndex = newlineIndexRaw === -1 ? -1 : newlineIndexRaw + 1;
|
|
804
|
-
|
|
805
|
-
// If there’s a multiline block comment, put everything _befor_ that
|
|
806
|
-
// comment in the specifiers’s `.after`.
|
|
807
|
-
const multilineBlockCommentIndex = after.findIndex(
|
|
808
|
-
(token2) => isBlockComment(token2) && hasNewline(token2.code),
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
const sliceIndex =
|
|
812
|
-
// If both a newline and a multiline block comment exists, choose the
|
|
813
|
-
// earlier one.
|
|
814
|
-
newlineIndex >= 0 && multilineBlockCommentIndex >= 0
|
|
815
|
-
? Math.min(newlineIndex, multilineBlockCommentIndex)
|
|
816
|
-
: newlineIndex >= 0
|
|
817
|
-
? newlineIndex
|
|
818
|
-
: multilineBlockCommentIndex >= 0
|
|
819
|
-
? multilineBlockCommentIndex
|
|
820
|
-
: // If there are no newlines, move the last whitespace into `result.after`.
|
|
821
|
-
endsWithSpaces(after)
|
|
822
|
-
? after.length - 1
|
|
823
|
-
: -1;
|
|
824
|
-
|
|
825
|
-
current.specifier = specifier;
|
|
826
|
-
current.after = sliceIndex === -1 ? after : after.slice(0, sliceIndex);
|
|
827
|
-
result.items.push(current);
|
|
828
|
-
result.after = sliceIndex === -1 ? [] : after.slice(sliceIndex);
|
|
829
|
-
|
|
830
|
-
break;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// If the last specifier has a trailing comma and all remaining whitespace
|
|
834
|
-
// and comments are on the same line we end up here. If so we want to move
|
|
835
|
-
// the final whitespace to `result.after`.
|
|
836
|
-
case "after": {
|
|
837
|
-
if (endsWithSpaces(current.after)) {
|
|
838
|
-
const last = current.after.pop();
|
|
839
|
-
result.after = [last];
|
|
840
|
-
}
|
|
841
|
-
result.items.push(current);
|
|
842
|
-
break;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// istanbul ignore next
|
|
846
|
-
default: {
|
|
847
|
-
throw new Error(`Unknown state: ${current.state}`);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
return result;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// If a specifier item starts with a line comment or a singleline block comment
|
|
855
|
-
// it needs a newline before that. Otherwise that comment can end up belonging
|
|
856
|
-
// to the _previous_ specifier after sorting.
|
|
857
|
-
function needsStartingNewline(tokens) {
|
|
858
|
-
const before = tokens.filter((token) => !isSpaces(token));
|
|
859
|
-
|
|
860
|
-
if (before.length === 0) {
|
|
861
|
-
return false;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const firstToken = before[0];
|
|
865
|
-
|
|
866
|
-
return (
|
|
867
|
-
isLineComment(firstToken) ||
|
|
868
|
-
(isBlockComment(firstToken) && !hasNewline(firstToken.code))
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function endsWithSpaces(tokens) {
|
|
873
|
-
const last = tokens.length > 0 ? tokens[tokens.length - 1] : undefined;
|
|
874
|
-
|
|
875
|
-
return last == null ? false : isSpaces(last);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
module.exports = {
|
|
879
|
-
extractChunks,
|
|
880
|
-
flatMap,
|
|
881
|
-
getImportExportItems,
|
|
882
|
-
isPunctuator,
|
|
883
|
-
maybeReportSorting,
|
|
884
|
-
printSortedItems,
|
|
885
|
-
printWithSortedSpecifiers,
|
|
886
|
-
sortImportExportItems,
|
|
887
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const natsort = require("natsort").default;
|
|
4
|
+
|
|
5
|
+
const NEWLINE = /(\r?\n)/;
|
|
6
|
+
|
|
7
|
+
const hasNewline = (string) => NEWLINE.test(string);
|
|
8
|
+
|
|
9
|
+
function guessNewline(sourceCode) {
|
|
10
|
+
const match = NEWLINE.exec(sourceCode.text);
|
|
11
|
+
|
|
12
|
+
return match == null ? "\n" : match[0];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseWhitespace(whitespace) {
|
|
16
|
+
const allItems = whitespace.split(NEWLINE);
|
|
17
|
+
|
|
18
|
+
// Remove blank lines. `allItems` contains alternating `spaces` (which can be
|
|
19
|
+
// the empty string) and `newline` (which is either "\r\n" or "\n"). So in
|
|
20
|
+
// practice `allItems` grows like this as there are more newlines in
|
|
21
|
+
// `whitespace`:
|
|
22
|
+
//
|
|
23
|
+
// [spaces]
|
|
24
|
+
// [spaces, newline, spaces]
|
|
25
|
+
// [spaces, newline, spaces, newline, spaces]
|
|
26
|
+
// [spaces, newline, spaces, newline, spaces, newline, spaces]
|
|
27
|
+
//
|
|
28
|
+
// If there are 5 or more items we have at least one blank line. If so, keep
|
|
29
|
+
// the first `spaces`, the first `newline` and the last `spaces`.
|
|
30
|
+
const items =
|
|
31
|
+
allItems.length >= 5
|
|
32
|
+
? [...allItems.slice(0, 2), ...allItems.slice(-1)]
|
|
33
|
+
: allItems;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
items
|
|
37
|
+
.map((spacesOrNewline, index) =>
|
|
38
|
+
index % 2 === 0
|
|
39
|
+
? { type: "Spaces", code: spacesOrNewline }
|
|
40
|
+
: { type: "Newline", code: spacesOrNewline },
|
|
41
|
+
)
|
|
42
|
+
// Remove empty spaces since it makes debugging easier.
|
|
43
|
+
.filter((token) => token.code !== "")
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const naturalSort = natsort();
|
|
48
|
+
function compare(path1, path2) {
|
|
49
|
+
const path1Depth = path1.split("-").filter((p) => p === "__").length;
|
|
50
|
+
const path2Depth = path2.split("-").filter((p) => p === "__").length;
|
|
51
|
+
const path1IsDot = path1 === "_-,";
|
|
52
|
+
const path2IsDot = path2 === "_-,";
|
|
53
|
+
|
|
54
|
+
if (path1IsDot && !path2IsDot) {
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
if (path2IsDot && !path1IsDot) {
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return path1Depth === path2Depth
|
|
62
|
+
? naturalSort(path1, path2)
|
|
63
|
+
: path2Depth - path1Depth;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isIdentifier = (node) => node.type === "Identifier";
|
|
67
|
+
|
|
68
|
+
const isKeyword = (node) => node.type === "Keyword";
|
|
69
|
+
|
|
70
|
+
const isPunctuator = (node, value) =>
|
|
71
|
+
node.type === "Punctuator" && node.value === value;
|
|
72
|
+
|
|
73
|
+
const isBlockComment = (node) => node.type === "Block";
|
|
74
|
+
|
|
75
|
+
const isLineComment = (node) => node.type === "Line";
|
|
76
|
+
|
|
77
|
+
const isSpaces = (node) => node.type === "Spaces";
|
|
78
|
+
|
|
79
|
+
const isNewline = (node) => node.type === "Newline";
|
|
80
|
+
|
|
81
|
+
const getImportExportKind = (node) =>
|
|
82
|
+
node.importKind || node.exportKind || "value";
|
|
83
|
+
|
|
84
|
+
function getSource(node) {
|
|
85
|
+
const source = node.source.value;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
// Sort by directory level rather than by string length.
|
|
89
|
+
source: source
|
|
90
|
+
// Treat `.` as `./`, `..` as `../`, `../..` as `../../` etc.
|
|
91
|
+
.replace(/^[./]*\.$/, "$&/")
|
|
92
|
+
// Make `../` sort after `../../` but before `../a` etc.
|
|
93
|
+
// Why a comma? See the next comment.
|
|
94
|
+
.replace(/^[./]*\/$/, "$&,")
|
|
95
|
+
// Make `.` and `/` sort before any other punctation.
|
|
96
|
+
// The default order is: _ - , x x x . x x x / x x x
|
|
97
|
+
// We’re changing it to: . / , x x x _ x x x - x x x
|
|
98
|
+
.replace(/[./_-]/g, (char) => {
|
|
99
|
+
switch (char) {
|
|
100
|
+
case ".": {
|
|
101
|
+
return "_";
|
|
102
|
+
}
|
|
103
|
+
case "/": {
|
|
104
|
+
return "-";
|
|
105
|
+
}
|
|
106
|
+
case "_": {
|
|
107
|
+
return ".";
|
|
108
|
+
}
|
|
109
|
+
case "-": {
|
|
110
|
+
return "/";
|
|
111
|
+
}
|
|
112
|
+
// istanbul ignore next
|
|
113
|
+
default: {
|
|
114
|
+
throw new Error(`Unknown source substitution character: ${char}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
118
|
+
originalSource: source,
|
|
119
|
+
kind: getImportExportKind(node),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Like `Array.prototype.findIndex`, but searches from the end.
|
|
124
|
+
function findLastIndex(array, fn) {
|
|
125
|
+
for (let index = array.length - 1; index >= 0; index--) {
|
|
126
|
+
if (fn(array[index], index, array)) {
|
|
127
|
+
return index;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// There are currently no usages of `findLastIndex` where nothing is found.
|
|
132
|
+
// istanbul ignore next
|
|
133
|
+
return -1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Like `Array.prototype.flatMap`, had it been available.
|
|
137
|
+
const flatMap = (array, fn) => array.flatMap(fn);
|
|
138
|
+
|
|
139
|
+
// Returns `sourceCode.getTokens(node)` plus whitespace and comments. All tokens
|
|
140
|
+
// have a `code` property with `sourceCode.getText(token)`.
|
|
141
|
+
function getAllTokens(node, sourceCode) {
|
|
142
|
+
const tokens = sourceCode.getTokens(node);
|
|
143
|
+
const lastTokenIndex = tokens.length - 1;
|
|
144
|
+
|
|
145
|
+
return flatMap(tokens, (token, tokenIndex) => {
|
|
146
|
+
const newToken = { ...token, code: sourceCode.getText(token) };
|
|
147
|
+
|
|
148
|
+
if (tokenIndex === lastTokenIndex) {
|
|
149
|
+
return [newToken];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const comments = sourceCode.getCommentsAfter(token);
|
|
153
|
+
const last = comments.length > 0 ? comments[comments.length - 1] : token;
|
|
154
|
+
const nextToken = tokens[tokenIndex + 1];
|
|
155
|
+
|
|
156
|
+
return [
|
|
157
|
+
newToken,
|
|
158
|
+
...flatMap(comments, (comment, commentIndex) => {
|
|
159
|
+
const previous =
|
|
160
|
+
commentIndex === 0 ? token : comments[commentIndex - 1];
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
...parseWhitespace(
|
|
164
|
+
sourceCode.text.slice(previous.range[1], comment.range[0]),
|
|
165
|
+
),
|
|
166
|
+
{ ...comment, code: sourceCode.getText(comment) },
|
|
167
|
+
];
|
|
168
|
+
}),
|
|
169
|
+
...parseWhitespace(
|
|
170
|
+
sourceCode.text.slice(last.range[1], nextToken.range[0]),
|
|
171
|
+
),
|
|
172
|
+
];
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Prints tokens that are enhanced with a `code` property – like those returned
|
|
177
|
+
// by `getAllTokens` and `parseWhitespace`.
|
|
178
|
+
const printTokens = (tokens) => tokens.map((token) => token.code).join("");
|
|
179
|
+
|
|
180
|
+
const removeBlankLines = (whitespace) =>
|
|
181
|
+
printTokens(parseWhitespace(whitespace));
|
|
182
|
+
|
|
183
|
+
// `comments` is a list of comments that occur before `node`. Print those and
|
|
184
|
+
// the whitespace between themselves and between `node`.
|
|
185
|
+
function printCommentsBefore(node, comments, sourceCode) {
|
|
186
|
+
const lastIndex = comments.length - 1;
|
|
187
|
+
|
|
188
|
+
return comments
|
|
189
|
+
.map((comment, index) => {
|
|
190
|
+
const next = index === lastIndex ? node : comments[index + 1];
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
sourceCode.getText(comment) +
|
|
194
|
+
removeBlankLines(sourceCode.text.slice(comment.range[1], next.range[0]))
|
|
195
|
+
);
|
|
196
|
+
})
|
|
197
|
+
.join("");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// `comments` is a list of comments that occur after `node`. Print those and
|
|
201
|
+
// the whitespace between themselves and between `node`.
|
|
202
|
+
const printCommentsAfter = (node, comments, sourceCode) =>
|
|
203
|
+
comments
|
|
204
|
+
.map((comment, index) => {
|
|
205
|
+
const previous = index === 0 ? node : comments[index - 1];
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
removeBlankLines(
|
|
209
|
+
sourceCode.text.slice(previous.range[1], comment.range[0]),
|
|
210
|
+
) + sourceCode.getText(comment)
|
|
211
|
+
);
|
|
212
|
+
})
|
|
213
|
+
.join("");
|
|
214
|
+
|
|
215
|
+
function getIndentation(node, sourceCode) {
|
|
216
|
+
const tokenBefore = sourceCode.getTokenBefore(node, {
|
|
217
|
+
includeComments: true,
|
|
218
|
+
});
|
|
219
|
+
if (tokenBefore == null) {
|
|
220
|
+
const text = sourceCode.text.slice(0, node.range[0]);
|
|
221
|
+
const lines = text.split(NEWLINE);
|
|
222
|
+
|
|
223
|
+
return lines[lines.length - 1];
|
|
224
|
+
}
|
|
225
|
+
const text = sourceCode.text.slice(tokenBefore.range[1], node.range[0]);
|
|
226
|
+
const lines = text.split(NEWLINE);
|
|
227
|
+
|
|
228
|
+
return lines.length > 1 ? lines[lines.length - 1] : "";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getTrailingSpaces(node, sourceCode) {
|
|
232
|
+
const tokenAfter = sourceCode.getTokenAfter(node, {
|
|
233
|
+
includeComments: true,
|
|
234
|
+
});
|
|
235
|
+
if (tokenAfter == null) {
|
|
236
|
+
const text = sourceCode.text.slice(node.range[1]);
|
|
237
|
+
const lines = text.split(NEWLINE);
|
|
238
|
+
|
|
239
|
+
return lines[0];
|
|
240
|
+
}
|
|
241
|
+
const text = sourceCode.text.slice(node.range[1], tokenAfter.range[0]);
|
|
242
|
+
const lines = text.split(NEWLINE);
|
|
243
|
+
|
|
244
|
+
return lines[0];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const sortImportExportItems = (items) =>
|
|
248
|
+
[...items].sort((itemA, itemB) =>
|
|
249
|
+
// If both items are side effect imports, keep their original order.
|
|
250
|
+
itemA.isSideEffectImport && itemB.isSideEffectImport
|
|
251
|
+
? itemA.index - itemB.index
|
|
252
|
+
: // If one of the items is a side effect import, move it first.
|
|
253
|
+
itemA.isSideEffectImport
|
|
254
|
+
? -1
|
|
255
|
+
: itemB.isSideEffectImport
|
|
256
|
+
? 1
|
|
257
|
+
: // Compare the `from` part.
|
|
258
|
+
compare(itemA.source.source, itemB.source.source) ||
|
|
259
|
+
// The `.source` has been slightly tweaked. To stay fully deterministic,
|
|
260
|
+
// also sort on the original value.
|
|
261
|
+
compare(itemA.source.originalSource, itemB.source.originalSource) ||
|
|
262
|
+
// Then put type imports/exports before regular ones.
|
|
263
|
+
compare(itemA.source.kind, itemB.source.kind) ||
|
|
264
|
+
// Keep the original order if the sources are the same. It’s not worth
|
|
265
|
+
// trying to compare anything else, and you can use `import/no-duplicates`
|
|
266
|
+
// to get rid of the problem anyway.
|
|
267
|
+
itemA.index - itemB.index,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const sortSpecifierItems = (items) =>
|
|
271
|
+
[...items].sort(
|
|
272
|
+
(itemA, itemB) =>
|
|
273
|
+
// Compare by imported or exported name (external interface name).
|
|
274
|
+
// import { a as b } from "a"
|
|
275
|
+
// ^
|
|
276
|
+
// export { b as a }
|
|
277
|
+
// ^
|
|
278
|
+
compare(
|
|
279
|
+
(itemA.node.imported || itemA.node.exported).name,
|
|
280
|
+
(itemB.node.imported || itemB.node.exported).name,
|
|
281
|
+
) ||
|
|
282
|
+
// Then compare by the file-local name.
|
|
283
|
+
// import { a as b } from "a"
|
|
284
|
+
// ^
|
|
285
|
+
// export { b as a }
|
|
286
|
+
// ^
|
|
287
|
+
compare(itemA.node.local.name, itemB.node.local.name) ||
|
|
288
|
+
// Then put type specifiers before regular ones.
|
|
289
|
+
compare(
|
|
290
|
+
getImportExportKind(itemA.node),
|
|
291
|
+
getImportExportKind(itemB.node),
|
|
292
|
+
) ||
|
|
293
|
+
// Keep the original order if the names are the same. It’s not worth
|
|
294
|
+
// trying to compare anything else, `import {a, a} from "mod"` is a syntax
|
|
295
|
+
// error anyway (but @babel/eslint-parser kind of supports it).
|
|
296
|
+
// istanbul ignore next
|
|
297
|
+
itemA.index - itemB.index,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// A “chunk” is a sequence of statements of a certain type with only comments
|
|
301
|
+
// and whitespace between.
|
|
302
|
+
function extractChunks(parentNode, isPartOfChunk) {
|
|
303
|
+
const chunks = [];
|
|
304
|
+
let chunk = [];
|
|
305
|
+
let lastNode;
|
|
306
|
+
|
|
307
|
+
for (const node of parentNode.body) {
|
|
308
|
+
const result = isPartOfChunk(node, lastNode);
|
|
309
|
+
switch (result) {
|
|
310
|
+
case "PartOfChunk": {
|
|
311
|
+
chunk.push(node);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case "PartOfNewChunk": {
|
|
316
|
+
if (chunk.length > 0) {
|
|
317
|
+
chunks.push(chunk);
|
|
318
|
+
}
|
|
319
|
+
chunk = [node];
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
case "NotPartOfChunk": {
|
|
324
|
+
if (chunk.length > 0) {
|
|
325
|
+
chunks.push(chunk);
|
|
326
|
+
chunk = [];
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// istanbul ignore next
|
|
332
|
+
default: {
|
|
333
|
+
throw new Error(`Unknown chunk result: ${result}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
lastNode = node;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (chunk.length > 0) {
|
|
341
|
+
chunks.push(chunk);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return chunks;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function maybeReportSorting(context, sorted, start, end) {
|
|
348
|
+
const sourceCode = context.getSourceCode();
|
|
349
|
+
const original = sourceCode.getText().slice(start, end);
|
|
350
|
+
if (original !== sorted) {
|
|
351
|
+
context.report({
|
|
352
|
+
messageId: "sort",
|
|
353
|
+
loc: {
|
|
354
|
+
start: sourceCode.getLocFromIndex(start),
|
|
355
|
+
end: sourceCode.getLocFromIndex(end),
|
|
356
|
+
},
|
|
357
|
+
fix: (fixer) => fixer.replaceTextRange([start, end], sorted),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function printSortedItems(sortedItems, originalItems, sourceCode) {
|
|
363
|
+
const newline = guessNewline(sourceCode);
|
|
364
|
+
|
|
365
|
+
const sorted = sortedItems
|
|
366
|
+
.map((groups) =>
|
|
367
|
+
groups
|
|
368
|
+
.map((groupItems) => groupItems.map((item) => item.code).join(newline))
|
|
369
|
+
.join(newline),
|
|
370
|
+
)
|
|
371
|
+
.join(newline + newline);
|
|
372
|
+
|
|
373
|
+
// Edge case: If the last import/export (after sorting) ends with a line
|
|
374
|
+
// comment and there’s code (or a multiline block comment) on the same line,
|
|
375
|
+
// add a newline so we don’t accidentally comment stuff out.
|
|
376
|
+
const flattened = flatMap(sortedItems, (groups) => groups.flat());
|
|
377
|
+
const lastSortedItem = flattened[flattened.length - 1];
|
|
378
|
+
const lastOriginalItem = originalItems[originalItems.length - 1];
|
|
379
|
+
const nextToken = lastSortedItem.needsNewline
|
|
380
|
+
? sourceCode.getTokenAfter(lastOriginalItem.node, {
|
|
381
|
+
includeComments: true,
|
|
382
|
+
filter: (token) =>
|
|
383
|
+
!isLineComment(token) &&
|
|
384
|
+
!(
|
|
385
|
+
isBlockComment(token) &&
|
|
386
|
+
token.loc.end.line === lastOriginalItem.node.loc.end.line
|
|
387
|
+
),
|
|
388
|
+
})
|
|
389
|
+
: undefined;
|
|
390
|
+
const maybeNewline =
|
|
391
|
+
nextToken != null &&
|
|
392
|
+
nextToken.loc.start.line === lastOriginalItem.node.loc.end.line
|
|
393
|
+
? newline
|
|
394
|
+
: "";
|
|
395
|
+
|
|
396
|
+
return sorted + maybeNewline;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Wrap the import/export nodes in `passedChunk` in objects with more data about
|
|
400
|
+
// the import/export. Most importantly there’s a `code` property that contains
|
|
401
|
+
// the node as a string, with comments (if any). Finding the corresponding
|
|
402
|
+
// comments is the hard part.
|
|
403
|
+
function getImportExportItems(
|
|
404
|
+
passedChunk,
|
|
405
|
+
sourceCode,
|
|
406
|
+
isSideEffectImport,
|
|
407
|
+
getSpecifiers,
|
|
408
|
+
) {
|
|
409
|
+
const chunk = handleLastSemicolon(passedChunk, sourceCode);
|
|
410
|
+
|
|
411
|
+
return chunk.map((node, nodeIndex) => {
|
|
412
|
+
const lastLine =
|
|
413
|
+
nodeIndex === 0
|
|
414
|
+
? node.loc.start.line - 1
|
|
415
|
+
: chunk[nodeIndex - 1].loc.end.line;
|
|
416
|
+
|
|
417
|
+
// Get all comments before the import/export, except:
|
|
418
|
+
//
|
|
419
|
+
// - Comments on another line for the first import/export.
|
|
420
|
+
// - Comments that belong to the previous import/export (if any) – that is,
|
|
421
|
+
// comments that are on the same line as the previous import/export. But
|
|
422
|
+
// multiline block comments always belong to this import/export, not the
|
|
423
|
+
// previous.
|
|
424
|
+
const commentsBefore = sourceCode
|
|
425
|
+
.getCommentsBefore(node)
|
|
426
|
+
.filter(
|
|
427
|
+
(comment) =>
|
|
428
|
+
comment.loc.start.line <= node.loc.start.line &&
|
|
429
|
+
comment.loc.end.line > lastLine &&
|
|
430
|
+
(nodeIndex > 0 || comment.loc.start.line > lastLine),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Get all comments after the import/export that are on the same line.
|
|
434
|
+
// Multiline block comments belong to the _next_ import/export (or the
|
|
435
|
+
// following code in case of the last import/export).
|
|
436
|
+
const commentsAfter = sourceCode
|
|
437
|
+
.getCommentsAfter(node)
|
|
438
|
+
.filter((comment) => comment.loc.end.line === node.loc.end.line);
|
|
439
|
+
|
|
440
|
+
const before = printCommentsBefore(node, commentsBefore, sourceCode);
|
|
441
|
+
const after = printCommentsAfter(node, commentsAfter, sourceCode);
|
|
442
|
+
|
|
443
|
+
// Print the indentation before the import/export or its first comment, if
|
|
444
|
+
// any, to support indentation in `<script>` tags.
|
|
445
|
+
const indentation = getIndentation(
|
|
446
|
+
commentsBefore.length > 0 ? commentsBefore[0] : node,
|
|
447
|
+
sourceCode,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Print spaces after the import/export or its last comment, if any, to
|
|
451
|
+
// avoid producing a sort error just because you accidentally added a few
|
|
452
|
+
// trailing spaces among the imports/exports.
|
|
453
|
+
const trailingSpaces = getTrailingSpaces(
|
|
454
|
+
commentsAfter.length > 0 ? commentsAfter[commentsAfter.length - 1] : node,
|
|
455
|
+
sourceCode,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const code =
|
|
459
|
+
indentation +
|
|
460
|
+
before +
|
|
461
|
+
printWithSortedSpecifiers(node, sourceCode, getSpecifiers) +
|
|
462
|
+
after +
|
|
463
|
+
trailingSpaces;
|
|
464
|
+
|
|
465
|
+
const all = [...commentsBefore, node, ...commentsAfter];
|
|
466
|
+
const [start] = all[0].range;
|
|
467
|
+
const [, end] = all[all.length - 1].range;
|
|
468
|
+
|
|
469
|
+
const source = getSource(node);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
node,
|
|
473
|
+
code,
|
|
474
|
+
start: start - indentation.length,
|
|
475
|
+
end: end + trailingSpaces.length,
|
|
476
|
+
isSideEffectImport: isSideEffectImport(node, sourceCode),
|
|
477
|
+
source,
|
|
478
|
+
index: nodeIndex,
|
|
479
|
+
needsNewline:
|
|
480
|
+
commentsAfter.length > 0 &&
|
|
481
|
+
isLineComment(commentsAfter[commentsAfter.length - 1]),
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Parsers think that a semicolon after a statement belongs to that statement.
|
|
487
|
+
// But in a semicolon-free code style it might belong to the next statement:
|
|
488
|
+
//
|
|
489
|
+
// import x from "x"
|
|
490
|
+
// ;[].forEach()
|
|
491
|
+
//
|
|
492
|
+
// If the last import/export of a chunk ends with a semicolon, and that
|
|
493
|
+
// semicolon isn’t located on the same line as the `from` string, adjust the
|
|
494
|
+
// node to end at the `from` string instead.
|
|
495
|
+
//
|
|
496
|
+
// In the above example, the import is adjusted to end after `"x"`.
|
|
497
|
+
function handleLastSemicolon(chunk, sourceCode) {
|
|
498
|
+
const lastIndex = chunk.length - 1;
|
|
499
|
+
const lastNode = chunk[lastIndex];
|
|
500
|
+
const [nextToLastToken, lastToken] = sourceCode.getLastTokens(lastNode, {
|
|
501
|
+
count: 2,
|
|
502
|
+
});
|
|
503
|
+
const lastIsSemicolon = isPunctuator(lastToken, ";");
|
|
504
|
+
|
|
505
|
+
if (!lastIsSemicolon) {
|
|
506
|
+
return chunk;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const semicolonBelongsToNode =
|
|
510
|
+
nextToLastToken.loc.end.line === lastToken.loc.start.line ||
|
|
511
|
+
// If there’s no more code after the last import/export the semicolon has to
|
|
512
|
+
// belong to the import/export, even if it is not on the same line.
|
|
513
|
+
sourceCode.getTokenAfter(lastToken) == null;
|
|
514
|
+
|
|
515
|
+
if (semicolonBelongsToNode) {
|
|
516
|
+
return chunk;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Preserve the start position, but use the end position of the `from` string.
|
|
520
|
+
const newLastNode = {
|
|
521
|
+
...lastNode,
|
|
522
|
+
range: [lastNode.range[0], nextToLastToken.range[1]],
|
|
523
|
+
loc: {
|
|
524
|
+
start: lastNode.loc.start,
|
|
525
|
+
end: nextToLastToken.loc.end,
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
return [...chunk.slice(0, lastIndex), newLastNode];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function printWithSortedSpecifiers(node, sourceCode, getSpecifiers) {
|
|
533
|
+
const allTokens = getAllTokens(node, sourceCode);
|
|
534
|
+
const openBraceIndex = allTokens.findIndex((token) =>
|
|
535
|
+
isPunctuator(token, "{"),
|
|
536
|
+
);
|
|
537
|
+
const closeBraceIndex = allTokens.findIndex((token) =>
|
|
538
|
+
isPunctuator(token, "}"),
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const specifiers = getSpecifiers(node);
|
|
542
|
+
|
|
543
|
+
if (
|
|
544
|
+
openBraceIndex === -1 ||
|
|
545
|
+
closeBraceIndex === -1 ||
|
|
546
|
+
specifiers.length <= 1
|
|
547
|
+
) {
|
|
548
|
+
return printTokens(allTokens);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const specifierTokens = allTokens.slice(openBraceIndex + 1, closeBraceIndex);
|
|
552
|
+
const itemsResult = getSpecifierItems(specifierTokens, sourceCode);
|
|
553
|
+
|
|
554
|
+
const items = itemsResult.items.map((originalItem, index) => ({
|
|
555
|
+
...originalItem,
|
|
556
|
+
node: specifiers[index],
|
|
557
|
+
}));
|
|
558
|
+
|
|
559
|
+
const sortedItems = sortSpecifierItems(items);
|
|
560
|
+
|
|
561
|
+
const newline = guessNewline(sourceCode);
|
|
562
|
+
|
|
563
|
+
// `allTokens[closeBraceIndex - 1]` wouldn’t work because `allTokens` contains
|
|
564
|
+
// comments and whitespace.
|
|
565
|
+
const hasTrailingComma = isPunctuator(
|
|
566
|
+
sourceCode.getTokenBefore(allTokens[closeBraceIndex]),
|
|
567
|
+
",",
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
const lastIndex = sortedItems.length - 1;
|
|
571
|
+
const sorted = flatMap(sortedItems, (item, index) => {
|
|
572
|
+
const previous = index === 0 ? undefined : sortedItems[index - 1];
|
|
573
|
+
|
|
574
|
+
// Add a newline if the item needs one, unless the previous item (if any)
|
|
575
|
+
// already ends with a newline.
|
|
576
|
+
const maybeNewline =
|
|
577
|
+
previous != null &&
|
|
578
|
+
needsStartingNewline(item.before) &&
|
|
579
|
+
!(
|
|
580
|
+
previous.after.length > 0 &&
|
|
581
|
+
isNewline(previous.after[previous.after.length - 1])
|
|
582
|
+
)
|
|
583
|
+
? [{ type: "Newline", code: newline }]
|
|
584
|
+
: [];
|
|
585
|
+
|
|
586
|
+
if (index < lastIndex || hasTrailingComma) {
|
|
587
|
+
return [
|
|
588
|
+
...maybeNewline,
|
|
589
|
+
...item.before,
|
|
590
|
+
...item.specifier,
|
|
591
|
+
{ type: "Comma", code: "," },
|
|
592
|
+
...item.after,
|
|
593
|
+
];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const nonBlankIndex = item.after.findIndex(
|
|
597
|
+
(token) => !isNewline(token) && !isSpaces(token),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
// Remove whitespace and newlines at the start of `.after` if the item had a
|
|
601
|
+
// comma before, but now hasn’t to avoid blank lines and excessive
|
|
602
|
+
// whitespace before `}`.
|
|
603
|
+
const after = item.hadComma
|
|
604
|
+
? nonBlankIndex === -1
|
|
605
|
+
? []
|
|
606
|
+
: item.after.slice(nonBlankIndex)
|
|
607
|
+
: item.after;
|
|
608
|
+
|
|
609
|
+
return [...maybeNewline, ...item.before, ...item.specifier, ...after];
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const maybeNewline =
|
|
613
|
+
needsStartingNewline(itemsResult.after) &&
|
|
614
|
+
!isNewline(sorted[sorted.length - 1])
|
|
615
|
+
? [{ type: "Newline", code: newline }]
|
|
616
|
+
: [];
|
|
617
|
+
|
|
618
|
+
return printTokens([
|
|
619
|
+
...allTokens.slice(0, openBraceIndex + 1),
|
|
620
|
+
...itemsResult.before,
|
|
621
|
+
...sorted,
|
|
622
|
+
...maybeNewline,
|
|
623
|
+
...itemsResult.after,
|
|
624
|
+
...allTokens.slice(closeBraceIndex),
|
|
625
|
+
]);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const makeEmptyItem = () => ({
|
|
629
|
+
// "before" | "specifier" | "after"
|
|
630
|
+
state: "before",
|
|
631
|
+
before: [],
|
|
632
|
+
after: [],
|
|
633
|
+
specifier: [],
|
|
634
|
+
hadComma: false,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Turns a list of tokens between the `{` and `}` of an import/export specifiers
|
|
638
|
+
// list into an object with the following properties:
|
|
639
|
+
//
|
|
640
|
+
// - before: Array of tokens – whitespace and comments after the `{` that do not
|
|
641
|
+
// belong to any specifier.
|
|
642
|
+
// - after: Array of tokens – whitespace and comments before the `}` that do not
|
|
643
|
+
// belong to any specifier.
|
|
644
|
+
// - items: Array of specifier items.
|
|
645
|
+
//
|
|
646
|
+
// Each specifier item looks like this:
|
|
647
|
+
//
|
|
648
|
+
// - before: Array of tokens – whitespace and comments before the specifier.
|
|
649
|
+
// - after: Array of tokens – whitespace and comments after the specifier.
|
|
650
|
+
// - specifier: Array of tokens – identifiers, whitespace and comments of the
|
|
651
|
+
// specifier.
|
|
652
|
+
// - hadComma: A Boolean representing if the specifier had a comma originally.
|
|
653
|
+
//
|
|
654
|
+
// We have to do carefully preserve all original whitespace this way in order to
|
|
655
|
+
// be compatible with other stylistic ESLint rules.
|
|
656
|
+
function getSpecifierItems(tokens) {
|
|
657
|
+
const result = {
|
|
658
|
+
before: [],
|
|
659
|
+
after: [],
|
|
660
|
+
items: [],
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
let current = makeEmptyItem();
|
|
664
|
+
|
|
665
|
+
for (const token of tokens) {
|
|
666
|
+
switch (current.state) {
|
|
667
|
+
case "before": {
|
|
668
|
+
switch (token.type) {
|
|
669
|
+
case "Newline": {
|
|
670
|
+
current.before.push(token);
|
|
671
|
+
|
|
672
|
+
// All whitespace and comments before the first newline or
|
|
673
|
+
// identifier belong to the `{`, not the first specifier.
|
|
674
|
+
if (result.before.length === 0 && result.items.length === 0) {
|
|
675
|
+
result.before = current.before;
|
|
676
|
+
current = makeEmptyItem();
|
|
677
|
+
}
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
case "Spaces":
|
|
682
|
+
case "Block":
|
|
683
|
+
case "Line": {
|
|
684
|
+
current.before.push(token);
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// We’ve reached an identifier.
|
|
689
|
+
default: {
|
|
690
|
+
// All whitespace and comments before the first newline or
|
|
691
|
+
// identifier belong to the `{`, not the first specifier.
|
|
692
|
+
if (result.before.length === 0 && result.items.length === 0) {
|
|
693
|
+
result.before = current.before;
|
|
694
|
+
current = makeEmptyItem();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
current.state = "specifier";
|
|
698
|
+
current.specifier.push(token);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
case "specifier": {
|
|
705
|
+
switch (token.type) {
|
|
706
|
+
case "Punctuator": {
|
|
707
|
+
// There can only be comma punctuators, but future-proof by checking.
|
|
708
|
+
// istanbul ignore else
|
|
709
|
+
if (isPunctuator(token, ",")) {
|
|
710
|
+
current.hadComma = true;
|
|
711
|
+
current.state = "after";
|
|
712
|
+
} else {
|
|
713
|
+
current.specifier.push(token);
|
|
714
|
+
}
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// When consuming the specifier part, we eat every token until a comma
|
|
719
|
+
// or to the end, basically.
|
|
720
|
+
default: {
|
|
721
|
+
current.specifier.push(token);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
case "after": {
|
|
728
|
+
switch (token.type) {
|
|
729
|
+
// Only whitespace and comments after a specifier that are on the same
|
|
730
|
+
// belong to the specifier.
|
|
731
|
+
case "Newline": {
|
|
732
|
+
current.after.push(token);
|
|
733
|
+
result.items.push(current);
|
|
734
|
+
current = makeEmptyItem();
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
case "Spaces":
|
|
739
|
+
case "Line": {
|
|
740
|
+
current.after.push(token);
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
case "Block": {
|
|
745
|
+
// Multiline block comments belong to the next specifier.
|
|
746
|
+
if (hasNewline(token.code)) {
|
|
747
|
+
result.items.push(current);
|
|
748
|
+
current = makeEmptyItem();
|
|
749
|
+
current.before.push(token);
|
|
750
|
+
} else {
|
|
751
|
+
current.after.push(token);
|
|
752
|
+
}
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// We’ve reached another specifier – time to process that one.
|
|
757
|
+
default: {
|
|
758
|
+
result.items.push(current);
|
|
759
|
+
current = makeEmptyItem();
|
|
760
|
+
current.state = "specifier";
|
|
761
|
+
current.specifier.push(token);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// istanbul ignore next
|
|
768
|
+
default: {
|
|
769
|
+
throw new Error(`Unknown state: ${current.state}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// We’ve reached the end of the tokens. Handle what’s currently in `current`.
|
|
775
|
+
switch (current.state) {
|
|
776
|
+
// If the last specifier has a trailing comma and some of the remaining
|
|
777
|
+
// whitespace and comments are on the same line we end up here. If so we
|
|
778
|
+
// want to put that whitespace and comments in `result.after`.
|
|
779
|
+
case "before": {
|
|
780
|
+
result.after = current.before;
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// If the last specifier has no trailing comma we end up here. Move all
|
|
785
|
+
// trailing comments and whitespace from `.specifier` to `.after`, and
|
|
786
|
+
// comments and whitespace that don’t belong to the specifier to
|
|
787
|
+
// `result.after`. The last non-comment and non-whitespace token is usually
|
|
788
|
+
// an identifier, but in this case it’s a keyword:
|
|
789
|
+
//
|
|
790
|
+
// export { z, d as default } from "a"
|
|
791
|
+
case "specifier": {
|
|
792
|
+
const lastIdentifierIndex = findLastIndex(
|
|
793
|
+
current.specifier,
|
|
794
|
+
(token2) => isIdentifier(token2) || isKeyword(token2),
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
const specifier = current.specifier.slice(0, lastIdentifierIndex + 1);
|
|
798
|
+
const after = current.specifier.slice(lastIdentifierIndex + 1);
|
|
799
|
+
|
|
800
|
+
// If there’s a newline, put everything up to and including (hence the `+
|
|
801
|
+
// 1`) that newline in the specifiers’s `.after`.
|
|
802
|
+
const newlineIndexRaw = after.findIndex((token2) => isNewline(token2));
|
|
803
|
+
const newlineIndex = newlineIndexRaw === -1 ? -1 : newlineIndexRaw + 1;
|
|
804
|
+
|
|
805
|
+
// If there’s a multiline block comment, put everything _befor_ that
|
|
806
|
+
// comment in the specifiers’s `.after`.
|
|
807
|
+
const multilineBlockCommentIndex = after.findIndex(
|
|
808
|
+
(token2) => isBlockComment(token2) && hasNewline(token2.code),
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
const sliceIndex =
|
|
812
|
+
// If both a newline and a multiline block comment exists, choose the
|
|
813
|
+
// earlier one.
|
|
814
|
+
newlineIndex >= 0 && multilineBlockCommentIndex >= 0
|
|
815
|
+
? Math.min(newlineIndex, multilineBlockCommentIndex)
|
|
816
|
+
: newlineIndex >= 0
|
|
817
|
+
? newlineIndex
|
|
818
|
+
: multilineBlockCommentIndex >= 0
|
|
819
|
+
? multilineBlockCommentIndex
|
|
820
|
+
: // If there are no newlines, move the last whitespace into `result.after`.
|
|
821
|
+
endsWithSpaces(after)
|
|
822
|
+
? after.length - 1
|
|
823
|
+
: -1;
|
|
824
|
+
|
|
825
|
+
current.specifier = specifier;
|
|
826
|
+
current.after = sliceIndex === -1 ? after : after.slice(0, sliceIndex);
|
|
827
|
+
result.items.push(current);
|
|
828
|
+
result.after = sliceIndex === -1 ? [] : after.slice(sliceIndex);
|
|
829
|
+
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// If the last specifier has a trailing comma and all remaining whitespace
|
|
834
|
+
// and comments are on the same line we end up here. If so we want to move
|
|
835
|
+
// the final whitespace to `result.after`.
|
|
836
|
+
case "after": {
|
|
837
|
+
if (endsWithSpaces(current.after)) {
|
|
838
|
+
const last = current.after.pop();
|
|
839
|
+
result.after = [last];
|
|
840
|
+
}
|
|
841
|
+
result.items.push(current);
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// istanbul ignore next
|
|
846
|
+
default: {
|
|
847
|
+
throw new Error(`Unknown state: ${current.state}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return result;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// If a specifier item starts with a line comment or a singleline block comment
|
|
855
|
+
// it needs a newline before that. Otherwise that comment can end up belonging
|
|
856
|
+
// to the _previous_ specifier after sorting.
|
|
857
|
+
function needsStartingNewline(tokens) {
|
|
858
|
+
const before = tokens.filter((token) => !isSpaces(token));
|
|
859
|
+
|
|
860
|
+
if (before.length === 0) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const firstToken = before[0];
|
|
865
|
+
|
|
866
|
+
return (
|
|
867
|
+
isLineComment(firstToken) ||
|
|
868
|
+
(isBlockComment(firstToken) && !hasNewline(firstToken.code))
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function endsWithSpaces(tokens) {
|
|
873
|
+
const last = tokens.length > 0 ? tokens[tokens.length - 1] : undefined;
|
|
874
|
+
|
|
875
|
+
return last == null ? false : isSpaces(last);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
module.exports = {
|
|
879
|
+
extractChunks,
|
|
880
|
+
flatMap,
|
|
881
|
+
getImportExportItems,
|
|
882
|
+
isPunctuator,
|
|
883
|
+
maybeReportSorting,
|
|
884
|
+
printSortedItems,
|
|
885
|
+
printWithSortedSpecifiers,
|
|
886
|
+
sortImportExportItems,
|
|
887
|
+
};
|